前言
在上一章《【项目实战】基于Agent的金融问答系统:Agent框架的构建》中,我们已经完成了基于agent的问答系统主流程构建,本章将介绍如何将Agent通过langserve部署为后端服务,同时基于Vue.js+Vite构建前端页面,完成问答系统前后端联调。
目标
- 将Agent部署为后端服务
- 构建前端页面,完成问答系统前后端联调
后端实现
代码实现
基于《大模型应用开发之Prompt初步了解》中langserve的所学内容,我们需要做的第一步为:
1、安装langserve依赖包:
pip install "langserve[all]"
2、创建一个server.py文件,具体如下;
代码文件:app/server.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from finance_bot_ex import FinanceBotEx
finance_bot_ex = FinanceBotEx()
# 创建 FastAPI 应用
app = FastAPI(
title="Qwen API",
version="0.1",
description="Qwen API",
)
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有的来源
allow_credentials=True,
allow_methods=["*"], # 允许的HTTP方法
allow_headers=["*"], # 允许的请求头
)
@app.post("/queryex", response_model=dict)
async def query(query: dict): # 使用字典类型代替Query模型
try:
# 从字典中获取input
input_data = query.get("input")
result = finance_bot_ex.handle_query(input_data)
# 返回字典格式的响应
return {"output": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 运行Uvicorn服务器
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8082)
代码说明:
-
首先,依赖包引入langserve的必要包:
from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware
-
其次,将我们之前封装好的FinanceBotEx类引入进来:
from finance_bot_ex import FinanceBotEx
-
然后,初始化FinanceBotEx类,并且创建FastAPI应用:
# 实例化FinanceBotEx类 finance_bot_ex = FinanceBotEx() # 创建 FastAPI 应用 app = FastAPI( title="Qwen API", version="0.1", description="Qwen API", ) # 添加 CORS 中间件(允许跨域访问) app.add_middleware( CORSMiddleware, allow_origins=["*"], # 允许所有的来源 allow_credentials=True, allow_methods=["*"], # 允许的HTTP方法 allow_headers=["*"], # 允许的请求头 )
-
最后,给定一个接口,接收用户输入,调用FinanceBotEx类的handle_query方法,返回结果:
@app.post("/queryex", response_model=dict) async def query(query: dict): # 使用字典类型代替Query模型 try: # 从字典中获取input input_data = query.get("input") result = finance_bot_ex.handle_query(input_data) # 返回字典格式的响应 return {"output": result} except Exception as e: raise HTTPException(status_code=500, detail=str(e))
服务部署
目前,整个工程目录结构如下:
smart-finance-bot \
|- app \ # 该目录用于服务端代码
|- conf
|- .qwen
|- utils \
|- util.py # 实现连接大模型的方法
|- rag \
|- pdf_processor.py
|- chroma_conn.py
|- test_framework.py
|- finance_bot_ex.py # 封装好的FinanceBotEx类
|- server.py # 新增的服务代码
启动后端服务方法
# 切换至项目根目录
cd smart-finance-bot
# 启动服务
python app/server.py
运行结果:
接口测试
服务正常启动后,我们通过curl命令测试后端接口正常
curl -X POST http://localhost:8082/queryex \
-H "Content-Type: application/json" \
-d '{
"config": {},
"input": "在20190211,按照中信行业分类的标准,一级行业中的\"A股公司数量\"最多的是\"机械\"行业,共有549家公司。"
}'
运行结果:
后端接口可以接收请求,服务一切正常,接下来我们开始构建前端页面,完成问答系统前后端联调。
前端实现
前端页面的实现方案有很多种,可以写个简单的HTML页面,也可以使用Vue.js框架来实现一个前端。
因为Vue.js是目前主流的前端框架,它有丰富的生态和API说明,在github上也有非常多的开源项目供参考,所以本例中我们将使用Vue.js框架来实现前端页面。
背景知识
Vue.js简介
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架。它的核心库专注于视图层,易于上手,并与其他库或现有项目进行整合。
Vue.js的特点
- 响应式数据绑定:Vue.js 提供了双向数据绑定,能够实时更新视图与数据之间的变化。
- 指令:Vue.js 提供了一些内置指令(如 v-bind、v-model、v-for 等),使得数据绑定和 DOM 操作变得简单直观。
- 生态系统:Vue.js 拥有丰富的生态系统,包括路由管理(Vue Router)、状态管理(Vuex)等工具,支持构建复杂的应用。
Vue.js的简单示例
<div id="app">
<h1>{{ message }}</h1>
<input v-model="message" placeholder="编辑我">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});
</script>
说明:
- 在上面的示例中,{{ message }} 用于显示数据,v-model 用于实现双向数据绑定,允许用户通过输入框修改 message 的内容。
- 更为详细的Vue.js使用方法,请查看官方文档地址:https://cn.vuejs.org/
代码实现
在本例中,我们参考Github上的一个开源项目进行二次开发,该项目git clone之后目录如下:
chatweb \
|- index.html # 前端入口文件
|- node_modules # 依赖包目录
|- package-lock.json # 锁定依赖版本的文件
|- package.json # 项目描述和依赖配置文件
|- postcss.config.js # PostCSS 配置文件
|- public # 公共资源目录
|- src # 源代码目录
|- App.vue # 主应用组件
|- assets # 资源文件目录
|- components # 组件目录
|- ChatComponents.vue # 聊天组件
|- Home.vue # 首页组件
|- Login.vue # 登录组件
|- NotFound.vue # 404 页面组件
|- main.js # 应用入口文件
|- routes # 路由配置目录
|- tailwind.config.js # Tailwind CSS 配置文件
|- tsconfig.json # TypeScript 配置文件
|- vite.config.js # Vite 配置文件
该项目是仿chatGPT的问答系统,仓库地址为:SinMu-L/chatweb
修改index.html
编辑index.html,修改如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="./src/assets/icon.jpg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>金融千问机器人</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
修改路由配置文件
编辑src/routes/routers.js,修改如下:
import ContainerTemplate from '../components/ChatComponents.vue'
import NotFound from '../components/NotFound.vue'
import Home from '../components/Home.vue'
var routes = [
{path: "/", name: 'root', component: Home },
{path: "/chat/:uuid?", component:ContainerTemplate, name:'chat'},
{ path: '/404', name: 'not_found', component: NotFound } // 捕获所有未匹配的路径
];
export default routes;
说明:
- 该配置文件用于控制页面请求的路由信息
- 当用户访问的路径为/时,将显示Home组件;
- 当用户访问的路径为/chat时,将显示ChatComponents组件;
- 当用户访问的路径为其他路径,将显示404页面。
由上也可以看到,后续修改的对话页面在 ChatComponents.vue
中。
修改聊天组件页面
由于原始的 ChatComponents.vue
代码不够规范,显示布局、显示样式以及逻辑代码没有分离,所以我们对其进行重构,重构内容分为三部分:
重构页面的显示布局
<template>
<div>
<div v-if="centerLodding" class="loading-container">
<n-spin size="large" />
</div>
<Login class="border border-red-400" />
<div class="flex flex-row h-min" :class="hasLogin('main')">
<!-- 非移动端模式下侧边栏的样式 -->
<div class="sidebar" :class="controlSidebarHidden ? 'w-0' : ''">
<div class="hidden sm:flex sm:flex-col sm:h-screen sm:border">
<!-- 新建按钮 -->
<div class="basis-1/12 flex justify-center items-center">
<n-button class="w-4/5" @click="addLeftListEle">新的会话</n-button>
</div>
<!-- 列表 -->
<div class="basis-10/12 overflow-auto border">
<div v-for="item in left_data.left_list" :key="item.uuid">
<router-link :to="`/chat/${item.uuid}`" class="sidebar-item">
<div class="w-4/5 flex items-center">
<n-icon size="medium">
<game-controller-outline />
</n-icon>
<div class="truncate mx-2" style="color: #000;">
<p v-if="!item.enable_edit" class="truncate h-full">{{ item.title }}</p>
<n-input v-else type="text" size="small" class="h-full" v-model:value="item.title" @keyup.enter="submit(item.uuid)" />
</div>
</div>
<div class="w-1/5 flex justify-center items-center" :class="route.params.uuid != item.uuid ? 'hidden' : ''">
<n-button-group size="small">
<n-button text @click="editLeftListEle(item.uuid)">
<n-icon><Edit /></n-icon>
</n-button>
<n-button text @click="delLeftListEle(item.uuid)">
<n-icon><Delete /></n-icon>
</n-button>
</n-button-group>
</div>
</router-link>
</div>
</div>
<!-- 设置页面 -->
<div class="footer flex flex-col items-center justify-between p-4 h-15">
<div class="user-info font-bold text-lg">金融千问机器人</div>
<div class="logo-container relative"> <!-- 添加一个容器 -->
<img src="../assets/background.png" alt="Logo" class="logo" />
</div>
<div class="robot-description text-sm text-gray-600 mt-2">
金融千问机器人,通过RAG对既有的PDF招股书建立了知识库,同时借助大模型+Agent对金融SQL数据库进行动态查询,旨在为用户提供快速、准确的金融信息和咨询服务。
</div>
<div class="settings-icon">
<n-icon @click="showSettingFunc()">
<SettingsOutline />
</n-icon>
</div>
</div>
</div>
</div>
<!-- 移动端模式下侧边栏的样式 -->
<div class="sm:hidden absolute top-1 left-1 h-full w-full flex flex-col">
<div>
<n-button text style="font-size:32px" @click="controlSidebarHidden = !controlSidebarHidden">
<n-icon class="text-black">
<Menu />
</n-icon>
</n-button>
</div>
<div v-if="!controlSidebarHidden" class="mobile-sidebar">
<div class="w-full flex flex-col h-screen border">
<div class="basis-1/12 flex justify-center items-center">
<n-button class="w-4/5" @click="addLeftListEle">新的对话</n-button>
</div>
<div class="basis-10/12 overflow-auto border">
<div v-for="item in left_data.left_list" :key="item.uuid">
<router-link :to="`/chat/${item.uuid}`" class="sidebar-item">
<div class="w-4/5 flex items-center">
<n-icon size="medium">
<game-controller-outline />
</n-icon>
<div class="truncate mx-2">
<p v-if="!item.enable_edit" class="truncate h-full">{{ item.title }}</p>
<n-input v-else type="text" size="small" class="h-full" v-model:value="item.title" @keyup.enter="submit(item.uuid)" />
</div>
</div>
<div class="w-1/5 flex justify-center items-center" :class="route.params.uuid != item.uuid ? 'hidden' : ''">
<n-button-group size="small">
<n-button text @click="editLeftListEle(item.uuid)">
<n-icon><Edit /></n-icon>
</n-button>
<n-button text @click="delLeftListEle(item.uuid)">
<n-icon><Delete /></n-icon>
</n-button>
</n-button-group>
</div>
</router-link>
</div>
</div>
<div class="footer flex items-center justify-between p-4 h-15">
<div class="user-info font-bold">金融千问机器人</div>
<div class="settings-icon">
<n-icon @click="showSettingFunc()">
<SettingsOutline />
</n-icon>
</div>
</div>
</div>
</div>
</div>
<div class="w-full sm:w-4/5 h-full">
<div class="flex flex-col h-screen">
<div v-if="left_list_is_empty" class="empty-chat-message">
Hi
</div>
<div v-else class="chat-area" id="msgArea">
<div v-for="(msglist, index) in getMsgList(route.params.uuid)" :key="index" class="flex flex-col mt-1 msgItem">
<div :class="msglist.reversion ? 'flex-row-reverse' : 'flex-row'" class="flex justify-start items-center h-10">
<img
class="rounded-full avatar"
:src="msglist.reversion ? userAvatar : robotAvatar"
alt="头像"
/>
<span class="ml-4 text-sm">{{ msglist.create_time }}</span>
</div>
<div class="flex" :class="msglist.reversion ? 'flex-row-reverse' : 'flex-row'">
<div class="message-container">
<n-spin v-if="msglist.msgload" size="small" stroke="red" />
<Markdown v-else :source="msglist.content"></Markdown>
</div>
</div>
</div>
</div>
<div v-if="!left_list_is_empty" class="input-area">
<div class="p-2">
<n-input-group>
<n-tooltip trigger="hover">
<template #trigger>
<n-button text size="large" class="px-2" @click="deleteChatItemHistory(route.params.uuid)">
<n-icon class="text-black">
<Delete />
</n-icon>
</n-button>
</template>
删除当前会话记录
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<n-button text size="large" class="pr-4" @click="dom2img()">
<n-icon class="text-black">
<Download />
</n-icon>
</n-button>
</template>
下载当前会话为图片
</n-tooltip>
<a href="" id="link" class="hidden"></a>
<n-input show-count @keyup.ctrl.enter="addMessageListItem(route.params.uuid)" placeholder="Ctrl+Enter 发送消息,发送消息长度需要大于2个字" v-model:value="input_area_value" type="textarea" size="tiny" :autosize="{ minRows: 2, maxRows: 5 }" />
<n-button ghost class="h-auto" @click="addMessageListItem(route.params.uuid)">
发送
</n-button>
</n-input-group>
</div>
</div>
</div>
</div>
</div>
<!-- 设置模态框 -->
<n-modal v-model:show="showSetting" style="width: 600px" class="custom-card" preset="card" title="设置">
<n-tabs type="line" animated>
<n-tab-pane name="about" tab="关于">
<div>这是一个demo项目,仅用于学习。</div>
<div class="my-4">技术栈:Vue3 + Vite + tailwindCss3 + NaiveUi</div>
</n-tab-pane>
<n-tab-pane name="settings" tab="设置">
<div class="grid grid-rows-3 gap-4">
<div>
<span class="mr-4">Server URL: </span>
<n-input v-model:value="setting.server_url" placeholder="请输入服务器 URL" />
</div>
<div>
<span class="mr-4">type: </span>
<n-select :style="{ width: '80%' }" :options="selectOptions" v-model:value="setting.type" />
</div>
<div class="flex justify-end">
<n-button
@click="confirmSettings"
class="custom-button">
确认
</n-button>
</div>
</div>
</n-tab-pane>
<n-tab-pane name="other" tab="其他">
其他
</n-tab-pane>
</n-tabs>
</n-modal>
</div>
</template>
重构页面的显示样式
<style scoped>
.router-link-active {
border-color: #18a058;
color: #18a058;
}
.loading-container {
position: absolute;
top: 33%;
left: 33%;
background-color: gray;
width: 33%;
height: 33%;
display: flex;
justify-content: center;
align-items: center;
}
.sidebar {
width: 20%;
height: 100%;
}
.sidebar-item {
margin: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid gray;
border-radius: 0.375rem; /* rounded-md */
padding: 0.5rem;
color: white;
}
.settings-container {
display: flex;
justify-content: flex-start;
align-items: center;
height: 50%;
}
.user-info {
width: 50%;
height: 100%;
display: grid;
grid-template-rows: repeat(2, 1fr);
}
.settings-icon {
width: 25%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.mobile-sidebar {
width: 80%;
background-color: white;
dark:bg-black;
}
.empty-chat-message {
flex-basis: 91%;
width: 100%;
padding: 3rem;
overflow: auto;
display: flex;
justify-content: center;
align-items: flex-start;
color: gray;
font-style: italic;
}
.chat-area {
flex-basis: 91%;
width: 100%;
padding: 3rem;
overflow: auto;
}
.message-container {
background-color: #bfdbfe; /* bg-blue-200 */
color: black; /* dark:text-black */
width: auto;
max-width: 80%;
min-width: 1%;
break-words: break-word;
overflow: hidden;
border-radius: 0.375rem; /* rounded-sm */
padding: 0.5rem;
margin: 0.25rem 0;
}
.input-area {
flex-basis: 8%;
width: 100%;
}
.avatar {
width: 50px;
height: 50px;
}
.footer {
text-align: center; /* 居中显示 */
}
.robot-description {
margin-top: 8px; /* 描述与 Logo 之间的间距 */
text-align: left; /* 左对齐 */
width: 100%; /* 使描述占满可用宽度 */
padding-left: 16px; /* 可选:增加左侧内边距 */
}
.logo-container {
position: relative; /* 设置容器为相对定位 */
}
.background-logo {
position: absolute; /* 设置背景图为绝对定位 */
top: 0; /* 顶部对齐 */
left: 0; /* 左侧对齐 */
width: 240px; /* 设置背景图宽度为240px */
height: auto; /* 保持比例 */
z-index: 1; /* 确保背景图在logo下方 */
}
.logo {
position: relative; /* 设置logo为相对定位 */
z-index: 2; /* 确保logo在背景图上方 */
width: 200px; /* 根据需要调整 Logo 大小 */
height: auto; /* 保持比例 */
top: 30%; /* 垂直居中 */
left: 50%; /* 水平居中 */
transform: translate(-50%, -50%); /* 使 logo 在背景图中心 */
}
.custom-button {
background-color: #007bff; /* 按钮背景色 */
color: white; /* 字体颜色 */
border: none; /* 去掉边框 */
padding: 10px 20px; /* 内边距 */
border-radius: 5px; /* 圆角 */
cursor: pointer; /* 鼠标样式 */
transition: background-color 0.3s; /* 添加过渡效果 */
}
.custom-button:hover {
background-color: #0056b3; /* 悬停时的背景颜色 */
}
</style>
重构页面的JS代码
<script setup>
import {
NButton, NInput, NIcon, NButtonGroup, NSpin,
NInputGroup, NCard, NModal, NTabs, NTabPane, NInputNumber, NSelect,
NTooltip,
useMessage
} from 'naive-ui'
import { GameControllerOutline, GameController } from '@vicons/ionicons5'
import { LogInOutline as LogInIcon, SettingsOutline, Menu } from '@vicons/ionicons5'
import userAvatar from '@/assets/user-avatar.png';
import robotAvatar from '@/assets/robot-avatar.png';
import { Edit, Delete, Download } from '@vicons/carbon'
import Markdown from 'vue3-markdown-it';
import Login from './Login.vue'
import { reactive, ref, getCurrentInstance, watch, watchEffect, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router'
import html2canvas from "html2canvas";
const router = useRouter()
const route = useRoute()
const instaceV = getCurrentInstance()
const message = useMessage()
import { onMounted } from 'vue';
// 在组件的 setup 函数中添加
onMounted(() => {
// 检查是否有会话记录
if (left_data.chat.length > 0) {
// 获取最后一个会话的 UUID
const lastChatUuid = left_data.chat[left_data.chat.length - 1].uuid;
// 路由跳转到最后一个会话
router.push({ name: 'chat', params: { uuid: lastChatUuid } });
} else {
// 如果没有会话,设置左侧列表为空
left_list_is_empty.value = true;
}
});
// 控制侧边栏显示隐藏
var controlSidebarHidden = ref(true)
// 移动端下侧边栏显影
const showSetting = ref(false)
var LLM_APIKEY = ref("sk-cwtdeSy4Ownmy6Uh5e9b6a67Fe4c4454A3Dc524876348eB1")
var setting = reactive({
server_url: 'http://localhost:8082/query', // 默认值
type: 'query',
})
let left_list_is_empty = ref(false)
var is404 = ref(false)
var segmented = {
content: 'soft',
footer: 'soft'
}
var selectOptions = ref([
{
label: 'Query式访问',
value: 'query'
},
{
label: '流式访问',
value: 'stream_log'
}
])
var centerLodding = ref(false)
var input_area_value = ref('')
var left_data = reactive({
left_list: [
// { uuid: 1, title: 'New Chat1', enable_edit: false },
// { uuid: 2, title: 'New Chat2', enable_edit: false },
],
chat: [
// {
// uuid: 1, msg_list: [
// { content: 'hello1', create_time: '2023-11-09 11:50:23', reversion: false, msgload: false },
// ]
// },
// {
// uuid: 2, msg_list: [
// { content: 'xxx', create_time: '2023-11-09 11:50:23', reversion: false, msgload: false },
// ]
// },
],
})
// 监听响应式数据
watch(left_data, (newValue, oldValue) => {
if (newValue.chat.length > 0) {
left_list_is_empty.value = false
} else {
left_list_is_empty.value = true
}
localStorage.setItem('chatweb', JSON.stringify(newValue))
})
// 创建响应式变量后只执行一次输出的需求
watchEffect(() => {
// 读取 localstorage
const data = localStorage.getItem('chatweb')
if (data) {
const history = JSON.parse(data)
left_data.left_list = history.left_list
left_data.chat = history.chat
} else {
left_list_is_empty.value = true
}
})
// 添加侧壁栏item
function addLeftListEle() {
const uuid = randomUuid()
left_data.left_list.push({
uuid: uuid,
title: `新的对话${uuid}`,
enable_edit: false
})
left_data.chat.push({
uuid: uuid,
msg_list: []
})
// 路由跳转到最新的item
router.push({ name: 'chat', params: { uuid: uuid } })
}
// 点击侧边栏某个item的编辑按钮
function editLeftListEle(uuid) {
const index = left_data.left_list.findIndex(v => v.uuid == uuid)
left_data.left_list[index].enable_edit = !left_data.left_list[index].enable_edit
// all_data.left_list[index].enable_edit = !all_data.left_list[index].enable_edit
// ls.updateLeftListItemEnableEditButton(uuid)
}
// 点击侧边栏某个item的删除按钮
async function delLeftListEle(uuid) {
var index = left_data.left_list.findIndex(v => v.uuid == uuid)
left_data.left_list.splice(index, 1)
const chat_index = left_data.chat.findIndex(v => v.uuid == uuid)
left_data.chat.splice(chat_index, 1)
if (left_data.chat.length == 0) {
left_list_is_empty = true
} else {
await nextTick()
// 默认跳转到最新的 chat
const last_chat_uuid = left_data.chat[left_data.chat.length - 1].uuid
router.push({ name: 'chat', params: { "uuid": last_chat_uuid } })
}
}
// 获取每个 chat 的msg list
function getMsgList(uuid) {
if (uuid && uuid != undefined) {
var index = left_data.chat.findIndex(v => v.uuid == uuid)
if (index == -1) {
return []
}
return left_data.chat[index].msg_list
}
return []
}
function randomUuid() {
var len = 9
var uuid = '';
for (let i = 0; i < len; i++) {
uuid += Math.floor(Math.random() * 10)
}
return Number(uuid, 10)
}
// 监听侧边栏item的回车事件
function submit(index) {
editLeftListEle(index)
}
// 发送消息
function addMessageListItem(uuid) {
if (input_area_value.value.length <= 2) {
message.info('内容长度不得小于2')
return false
}
var index = left_data.chat.findIndex(v => v.uuid == uuid)
const now_t = (new Date()).toLocaleString('sv-SE', { "timeZone": "PRC" })
left_data.chat[index].msg_list.push({
content: input_area_value.value,
create_time: now_t,
reversion: true,
msgload: false
})
const body = {
config: {},
input: input_area_value.value// 按要求的格式拼接
};
// 清空编辑框
input_area_value.value = ''
var ele = document.getElementById("msgArea")
ele.scrollTop = ele.scrollHeight + ele.offsetHeight
// 添加一个占位消息,表示正在加载
left_data.chat[index].msg_list.push({
content: '',
create_time: now_t,
reversion: false,
msgload: true
})
console.info("开始发送消息...")
// 使用编辑框中的 URL 进行请求
const url = setting.server_url; // 获取 URL
if (setting.type === 'stream_log') {
startStream(index, body, url); // 传递 URL
} else {
startRequest(index, body, url); // 传递 URL
}
}
function buildMessagePromt(index) {
const res = []
left_data.chat[index].msg_list.forEach(v => {
let role = v.reversion ? 'user' : 'assistant'
res.push({
role: role,
content: v.content
})
})
res.pop()
return res
}
async function startRequest(index, body, url) {
const key = LLM_APIKEY.value;
let response = null;
try {
response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json", // 使用 JSON 作为接受格式
"Authorization": `Bearer ${key}`, // 如果需要 API 密钥
},
mode: "cors",
body: JSON.stringify(body),
});
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].msgload = false;
} catch (error) {
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content += `发生了一些错误:${error.message}`;
return false;
}
if (response.status !== 200) {
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content += `发生了一些错误:${response.status}-${response.statusText}`;
return false;
}
try {
const data = await response.json();
if (data && data.output) { // 检查数据是否存在并包含 output
const outputText = data.output;
const currentContent = left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content;
if (!currentContent.includes(outputText)) {
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content += outputText;
}
} else {
throw new Error('返回的数据结构不符合预期');
}
} catch (e) {
console.error('Error parsing JSON:', e);
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content += '解析响应时发生错误';
}
return true; // 返回成功状态
}
async function startStream(index, body, url) {
let response = null;
try {
response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9"
},
mode: "cors",
body: JSON.stringify(body),
});
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].msgload = false;
} catch (error) {
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content += `发生了一些错误:${error.message}`;
return false;
}
if (response.status !== 200) {
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content += `发生了一些错误:${response.status}-${response.statusText}`;
return false;
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
const readStream = async () => {
const { done, value } = await reader.read();
if (done) {
console.log('Stream reading complete');
return;
}
buffer += decoder.decode(value, { stream: true });
let completeData = '';
let separatorIndex;
while ((separatorIndex = buffer.indexOf('\n')) !== -1) {
completeData = buffer.slice(0, separatorIndex).trim();
buffer = buffer.slice(separatorIndex + 1);
if (completeData.startsWith('data: ')) {
const jsonString = completeData.slice(6); // 去掉 'data: '
try {
const data = JSON.parse(jsonString);
data.ops.forEach(op => {
if (op.op === 'add' && op.path.includes('/streamed_output')) {
const outputText = op.value;
if (outputText) {
const currentContent = left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content;
if (!currentContent.includes(outputText)) {
left_data.chat[index].msg_list[left_data.chat[index].msg_list.length - 1].content += outputText;
}
}
}
});
} catch (e) {
console.error('Error parsing JSON:', e, 'Complete Data:', completeData);
}
} else {
console.log('No valid JSON found in:', completeData);
}
}
return readStream();
};
return readStream();
}
function hasLogin(compomentName = 'Login') {
if (compomentName == 'login') return instaceV.proxy.hasLogin ? 'hidden' : '';
if (compomentName == 'main') return instaceV.proxy.hasLogin ? '' : 'hidden';
}
function showSettingFunc() {
showSetting.value = !showSetting.value
}
// 删除当前会话记录
function deleteChatItemHistory(uuid) {
const index = left_data.chat.findIndex(v => v.uuid == uuid);
left_data.chat[index].msg_list = []
message.success('当前会话记录已清理')
}
function confirmSettings() {
// 这里可以添加保存设置的逻辑
console.log("设置已确认:", setting);
// 例如,你可以关闭模态框
showSetting.value = false;
}
// 当前会话下载为图片
async function dom2img() {
centerLodding.value = true
var ele = document.querySelectorAll(".msgItem")
var msgAreaDom = document.getElementById("msgArea")
const width = msgAreaDom.offsetWidth * 2
const height = msgAreaDom.scrollHeight * 1.5
let canvas1 = document.createElement('canvas');
let context = canvas1.getContext('2d');
canvas1.width = width;
canvas1.height = height;
// 绘制矩形添加白色背景色
context.rect(0, 0, width, height);
context.fillStyle = "#fff";
context.fill();
let beforeHeight = 0
for (let i = 0; i < ele.length; i++) {
const dom_canvas = await html2canvas(ele[i], {
scrollX: 0,
scrollY: 0,
height: ele[i].scrollHeight,
width: ele[i].scrollWidth,
})
// var image = dom_canvas.toDataURL("image/png");
context.drawImage(dom_canvas, 0, beforeHeight, dom_canvas.width, dom_canvas.height)
beforeHeight = beforeHeight + dom_canvas.height;
}
var image = canvas1.toDataURL("image/png").replace("image/png", "image/octet-stream");
var link = document.getElementById("link");
link.setAttribute("download", `chatweb-${(new Date()).getTime()}.png`);
link.setAttribute("href", image);
link.click();
centerLodding.value = false
message.success('图片下载完成')
}
</script>
服务部署
# 切换目录到chatweb目录
cd chatweb
# 安装依赖的组件
npm install
# 启动服务
npm run dev
运行效果:
接口联调
接下来,通过浏览器的开发者工具进行接口调试
-
在浏览器中打开 http://localhost:5173/
-
点击开始按钮切换页面至聊天页面,打开浏览器的开发者工具
-
输入问题后点击发送,观察右侧开发者工具的Console面板,观察请求发送情况,以及返回的响应数据。
具体调试遇到的问题不尽相同,所以本章不再赘述具体调试方式,请读者根据问题自行调试。
待整体调试通过之后,问答系统的前后端即完成主流程。
内容小结
- 后端服务可以使用langserve部署
- 第一步:使用langserve时,需要pip install langserve的相关组件
- 第二步:创建对应的server.py文件,在代码中添加FastAPI的API接口
- 第三步:通过python server.py启动服务,通过浏览器访问对应地址即可
- 第四步:为了便于快速定位问题,可以通过curl 命令进行后端接口测试
- 前端服务可以使用vue.js进行快速开发
- 第一步:为了快速搭建前端页面,我们可以在github上搜索相关的vue.js开源项目
- 第二步:基于开源项目,进行快速搭建,并添加自己的业务逻辑
- 第三步:通过浏览器调试,观察请求和响应情况,调试问题
附录
欢迎查看该系列的其他文章:
- 【项目实战】基于Agent的金融问答系统:项目简介
- 【项目实战】基于Agent的金融问答系统:RAG检索模块初建成
- 【项目实战】基于Agent的金融问答系统:Agent框架的构建
- 【项目实战】基于Agent的金融问答系统:前后端流程打通
- 【项目实战】基于Agent的金融问答系统:代码重构
- 【项目实战】基于Agent的金融问答系统:RAG的检索增强之ElasticSearch
- 【项目实战】基于Agent的金融问答系统:RAG的检索增强之上下文重排和压缩
欢迎关注公众号以获得最新的文章和新闻