一、需求背景
在资本退去后,现如今的互联网行情很差劲,很多创新业务都不得不砍除。再加上国内互联网时代进入到了一个增量犹显疲态,增量杀红了眼!
阶段,各大互联网公司均有一种断臂求生的态势!各位互联网同行,大力发展第二职业已然变成了必要的生产力!
我们既要辩证的看待事物,也要符合规定去看待!作为法律门外汉的我们,从零去学习庞大的法律知识已然是不切实际!单独去聘专业的法律专家或许又流程繁琐!所以,就让我们拿起ChatGPT
,做一个AI版的法律质询顾问,解答你的全部法律问题!
二、项目原理及架构
2.1 实现原理
将CahtGPT
的角色设置为专业法律顾问,让他根据我们所提供的实际案例来产生相应的专业法律内容的回复,所以核心的一点就是我们需要写好相应的Prompt提示词内容
,根据Prompt提示词的设计原则:在OpenAI接口所允许的Tokens数量限制下,为AI模型提供尽可能丰富的内容引导,也就是使用Few-Shot机制完成AI模型角色的预设!
具体使用如下:
你是一个研究劳动法的专业律师,从现在开始你需要根据我的问题从劳动法的角度来分析我的案件,并且给出我相应的专业建议。我的问题是:我被裁员了该怎么办?
2.2 技术架构
本程序的核心在于构建优值的Prompt提示词
+优化Token的消费接口
,剩余的都是我们的一些接口的增删改查以及前端流式数据的输出与展示。
2.3 技术栈
模块 | 语言及框架 | 涉及的技术要点 |
---|---|---|
小程序前端 | 基于VUE 2.0语法 +Uni-app跨平台开发框架 | Http接口通信、Flex布局方式、uView样式库的使用、JSON数据解析、定时器的使用 |
小程序接口服务端 | Python + Flask WEB框架 | rest接口的开发、 ChatGPT API接口的数据对接 、 前后端websocket实时通信 |
2.4 数据交互原理
三、项目功能的实现
3.1 ChatGPT API的接入
要接入ChatGPT API,需要按照以下步骤进行操作:
- 注册一个账号并登录到OpenAI的官网:https://openai.com/
- 在Dashboard页面上,创建一个API密钥。在“API Keys”选项卡下,点击“Generate New Key”按钮。将生成的密钥保存好,以备后续使用。
- 选择所需的API服务,例如“Completion” API,以使用OpenAI的文本生成功能。
使用Python
调用ChatGPT API
实现代码如下:
- 方法一:使用
request
库
import requests
import json
# 构建API请求
url = "https://api.openai.com/v1/engines/davinci-codex/completions"
headers = {"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_KEY"}
data = {
"prompt": "Hello, my name is",
"max_tokens": 5
}
# 发送API请求
response = requests.post(url, headers=headers, data=json.dumps(data))
# 解析API响应
response_data = json.loads(response.text)
generated_text = response_data["choices"][0]["text"]
print(generated_text)
- 方式二:使用
openAI库
from flask import Flask, request
import openai
app = Flask(__name__)
openai.api_key = "YOUR_API_KEY_HERE"
@app.route("/")
def home():
return "Hello, World!"
@app.route("/chat", methods=["POST"])
def chat():
data = request.json
response = openai.Completion.create(
engine="davinci",
prompt=data["message"],
max_tokens=60
)
return response.choices[0].text
if __name__ == "__main__":
app.run()
3.2 小程序端设计与实现
法律引导界面 | 法律质询界面 |
---|---|
咨询历史 | 付费质询记录 |
---|---|
3.3 小程序前端流式输出设计与实现
通过查阅ChatGPT官网文档
我们可以知道:·chatGPT是支持流式数据的返回·
,而我们作为一个API的调用方,我们起到的只是一个接口数据的获取和处理的作用,ChatGPT所提供的流式数据返回也只是对于后端调用接口服务时有效,而对于我们的小程序前端就需要在数据返回接口中使用到数据流式响应的技术:正是由于Http是无状态的协议,前端在发送完请求之后,后端无法继续唤起上一次的调用方,也就无法保持流式数据的响应。
所以我们这里使用WebSocket长链接技术
以实现数据的流式响应!
- CahtGPT法律咨询完整流式连接示例代码:
<template>
<view class="content">
<view class="body">
<view v-for="(item, index) in arr" :key="index" :id="'message' + item.message_id">
<view class="body_l" v-if="item.role == 'assistant'">
<view class="body_lpic a">
<image :src="now_model.icon" mode=""></image>
</view>
<view class="body_box">
<view class="body_jt">
<u-icon name="play-left-fill" color="#F6F7FB" size="20"></u-icon>
</view>
<!-- <view class="body_lcon pdg" v-html="item.content"></view> -->
<view class="body_rcon pdg">
<zero-markdown-view :markdown="item.content"></zero-markdown-view>
</view>
<view style="position: absolute;right: 0;bottom: 10rpx;">
<u-icon @click="copy(item)" name="file-text-fill" size="30"></u-icon>
</view>
</view>
</view>
<view class="body_r" v-if="item.role == 'user'">
<view class="body_box">
<view class="body_rjt">
<u-icon name="play-right-fill" color="#F6F7FB" size="20"></u-icon>
</view>
<!-- <view class="body_rcon pdg" v-html="item.content"> -->
<view class="body_rcon pdg">
<zero-markdown-view :markdown="item.content"></zero-markdown-view>
</view>
</view>
<view class="body_rpic a">
<!-- <image v-if="token != ''" :src="vals.avatar" mode="aspectFill"></image> -->
<image :src="userInfo.avatar ? userInfo.avatar : '../../static/head.png'" mode="aspectFill">
</image>
</view>
</view>
</view>
</view>
<view class="body_l" v-if="loading" style="margin-left: 30rpx;" id="loading">
<view class="body_lpic a">
<image :src="now_model.icon" mode=""></image>
</view>
<view class="body_box ">
<view class="body_lcon pdg" style="display: flex;overflow-x: hidden;">
<u-loading-icon :show="loading"></u-loading-icon>
<view v-if="!typeingText" style="padding: 30rpx;">{{ now_model.title }}思考中...</view>
<!-- <zero-markdown-view v-if="!typeingText" style="padding: 0 20rpx;" :markdown="now_model.title">思考中...</zero-markdown-view> -->
<!-- <view v-else style="padding: 30rpx;">{{ typeingText }}...</view> -->
<zero-markdown-view v-else style="padding: 0 20rpx;" :markdown="typeingText"></zero-markdown-view>
</view>
</view>
</view>
<view class="flooer">
<view class="flooer_l">
<u--textarea v-model="prompt" placeholder="请输入内容,开始体验与人工智能交流吧" border="none" autoHeight
style="background-color: #F6F7FB;" :maxlength="256" holdKeyboard :showConfirmBar="false"
:cursorSpacing="28"></u--textarea>
<!-- <u--input placeholder="请输入内容,开始体验与人工智能交流吧" border="none" clearable v-model="prompt"
placeholderStyle="color: #7F8084"></u--input> -->
</view>
<view class="flooer_r pdg" @click="sendout">
<view class="flooer_rpic a">
<image src="../../static/fasong.png" mode=""></image>
</view>
</view>
</view>
<view style="height: 108rpx;"></view>
</view>
</template>
<script>
import setting from "@/common/config";
//markdown相关插件
// import markdownFunc from '@/uni_modules/jo-markdown/components/jo-markdown/index.js';
// #ifdef H5
import axios from 'axios'
// #endif
import {
mapState
} from "vuex"
var that;
export default {
data() {
return {
prompt: '',
arr: [],
timer: null,
loading: false,
chat_id: '',
typeingText: "",
typeingId: "",
now_model: {},
}
},
computed: {
...mapState(['config', 'num', 'userInfo'])
},
onLoad(op) {
if (op.model) {
this.now_model = JSON.parse(op.model)
} else {
this.now_model = this.$store.state.main_model
}
uni.setNavigationBarTitle({
title: this.now_model.title
})
this.arr.push({
role: 'assistant',
content: this.now_model.first_message,
message_id: 'aa'
})
let that = this;
if (op.chat_id) {
this.chat_id = op.chat_id;
this.getMessages()
}
let pong = '';
let userInfo = uni.getStorageSync('userInfo')
uni.connectSocket({
url: setting.ws
});
uni.onSocketOpen(function (res) {
console.log('chat页WebSocket连接已打开!');
// 绑定UID
sendMsg({
type: 'login',
uid: userInfo.member_id
// uid: 3
})
// 设置心跳包
pong = setInterval(() => {
sendMsg({
type: 'pong'
})
}, 20 * 1000);
});
uni.onSocketError(function (res) {
console.log('WebSocket连接打开失败,请检查!');
});
uni.onSocketMessage(function (res) {
// console.log('收到服务器内容:' + res.data);
let json = JSON.parse(res.data)
// console.log(json)
if (json.hasOwnProperty('id')) {
that.typeingId = json.id
if (!json.choices[0].delta.finish_reason) {
if (json.choices[0].delta.hasOwnProperty('content')) {
that.typeingText += json.choices[0].delta.content
// 此处自行斟酌是否替换
// that.typeingText = that.typeingText.replace(/[cogptn]+/ig, "ChatGLM-6B")
}
}
}
});
// socket断开时通知后端改变登录状态
uni.onSocketClose(function (res) {
console.log('WebSocket 已关闭!');
});
//上文中的sendMsg原型
const sendMsg = function (data) {
uni.sendSocketMessage({
data: JSON.stringify(data)
})
}
},
onUnload() {
uni.closeSocket()
},
onShow() {
that = this;
uni.getStorage({
key: 'token',
success: function (res) {
that.token = res.data;
},
fail() {
that.token = '';
}
});
},
onBackPress() {
uni.hideLoading()
clearInterval(this.timer);
},
onHide() {
uni.hideLoading()
clearInterval(this.timer);
},
methods: {
getMessages() {
uni.showLoading({
})
this.$request("/api/Message/index", "POST", {
chat_id: this.chat_id,
limit: 200,
sort: 'message_id',
order: 'asc',
}).then((res) => {
console.log(res);
if (res.status == 200) {
this.arr = this.arr.concat(res.data.data)
this.$nextTick(() => {
uni.pageScrollTo({
selector: "#message" + this.arr[this.arr.length - 1].message_id,
duration: 100
});
});
uni.hideLoading()
} else {
this.$failToast('服务器繁忙,请稍后再试');
}
});
},
sendout() {
if (!this.prompt) {
this.$failToast("请输入内容");
return;
}
this.loading = true;
this.arr.push({
role: 'user',
content: this.prompt,
message_id: this.arr.length + 1
});
let message = this.prompt;
this.prompt = ""
this.$nextTick(() => {
uni.pageScrollTo({
selector: "#loading",
duration: 100
});
});
setTimeout(() => {
this.sub(message)
}, 200);
},
sub(message) {
// 条件编译处理,h5端uni.request与socket会互相阻塞
// #ifndef H5
this.$request("/api/Message/sendV2", "POST", {
message: message,
chat_id: this.chat_id,
model_id: this.now_model.assistant_id
// member_id: 3 // 调试之用,线上环境为token解码
}).then((res) => {
console.log(res);
if (res.status == 200) {
this.loading = false;
this.typeingText = "";
this.typeingId = "";
this.arr.push({
role: res.data.role,
// 此处自行斟酌是否替换
content: res.data.content,
// content: res.data.content.replace(/[cogptn]+/ig, "ChatGLM-6B")
});
if (!this.chat_id) {
this.chat_id = res.data.chat_id
}
this.$store.commit('useNum', 1);
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 20000,
duration: 100
});
});
} else if (res.status == 901) {
this.loading = false;
this.$failToast('服务器繁忙,请稍后再试');
} else if (res.status == 411) {
this.loading = false;
uni.showModal({
content: res.msg
})
} else {
this.loading = false;
this.$failToast(res.msg);
}
}).catch(err => {
console.log(err)
this.loading = false;
this.$failToast(err.errMsg);
})
// #endif
// #ifdef H5
axios({
method: 'post',
url: setting.base_url + "/api/Message/sendV2",
data: {
message: message,
chat_id: this.chat_id,
model_id: this.now_model.assistant_id
},
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
'Authorization': this.$store.state.token
}
}).then(response => {
console.log(response)
let res = response.data
if (res.status == 200) {
this.loading = false;
this.typeingText = "";
this.typeingId = "";
this.arr.push({
role: res.data.role,
content: res.data.content
});
if (!this.chat_id) {
this.chat_id = res.data.chat_id
}
this.$store.commit('useNum', 1);
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 20000,
duration: 100
});
});
} else if (res.status == 901) {
this.loading = false;
this.$failToast('服务器繁忙,请稍后再试');
} else if (res.status == 411) {
this.loading = false;
uni.showModal({
content: res.msg
})
} else {
this.loading = false;
this.$failToast(res.msg);
}
}).catch(error => {
// reject(error);
});
// #endif
},
copy(item) {
uni.setClipboardData({
data: item.content,
success() {
uni.showModal({
title: '提示',
content: '内容已复制到系统剪贴板'
})
}
})
}
}
}
</script>
<style>
@import '../../components/css/index.css';
</style>
3.4 法律小程序界面实现
<template>
<view class="content">
<!-- 图片区域 -->
<view style="margin:0;padding:0;padding-top:-10rpx">
<image src="/static/images/banner.png" mode="widthFix" style="width:100%;display: block;"></image>
</view>
<!-- 咨询区域 -->
<u-row :gutter="0">
<u-col :span="6">
<u-card :show-head="false" :border-radius="18" box-shadow="7rpx 8rpx 20rpx #ddd">
<view slot="body" @click="toChat">
<view>
<text class="consult">AI咨询</text>
</view>
<view>
<u-row :gutter="0">
<u-col :span="9" style="margin:0;padding:0;margin-bottom:20rpx;">
<text class="consult-sub">CahtGPT智能AI咨询</text>
</u-col>
<u-col :span="2" style="margin:0;padding:0">
<view style="position:absolute;right:5rpx;bottom:5rpx">
<image src="/static/images/chat_consult.png" mode="widthFix"
style="width:110rpx;"></image>
</view>
</u-col>
</u-row>
</view>
</view>
</u-card>
</u-col>
<u-col :span="6">
<u-card :show-head="false" :border-radius="18" box-shadow="7rpx 8rpx 20rpx #ddd">
<view slot="body">
<view>
<text class="consult">真人咨询</text>
</view>
<view>
<u-row :gutter="0">
<u-col :span="9" style="margin:0;padding:0;margin-bottom:20rpx">
<text class="consult-sub">专业律师在线服务</text>
</u-col>
<u-col :span="2" style="margin:0;padding:0;">
<view style="position:absolute;right:5rpx;bottom:5rpx">
<image src="/static/images/phone_consult.png" mode="widthFix"
style="width:110rpx;"></image>
</view>
</u-col>
</u-row>
</view>
</view>
</u-card>
</u-col>
</u-row>
<!-- 按类型咨询版块 -->
<u-row style="margin-top:15rpx">
<u-col :span="12">
<u-section font-size="35" lineColor="#5b80f6" title="按类型咨询" :right="false"></u-section>
</u-col>
</u-row>
<view style="margin-left:2%;margin-right:2%;">
<u-row gutter="0" style="margin-top:5rpx;">
<u-col :span="3" v-for="(caseType,index) in caseTypeList" :key="index">
<view class="case-type">
<view style="margin-bottom:10rpx;">
<image :src="caseType.caseTypeIcon" mode="widthFix" style="width:55rpx"></image>
</view>
<view>
<text>{{caseType.caseTypeName}}</text>
</view>
</view>
</u-col>
</u-row>
</view>
<!-- 免费咨询 -->
<view style="margin-top:30rpx;">
<u-row>
<u-col :span="12">
<u-section font-size="35" lineColor="#5b80f6" title="免费咨询" sub-title="我要提问"></u-section>
</u-col>
</u-row>
</view>
<view style="margin-left:2%;margin-right:2%;margin-top:20rpx;">
<u-cell-group>
<u-cell-item title="法律问题"></u-cell-item>
<u-cell-item title="法律问题"></u-cell-item>
</u-cell-group>
</view>
<!-- 法律知识 -->
<view style="margin-top:30rpx;">
<u-row>
<u-col :span="12">
<u-section font-size="35" lineColor="#5b80f6" title="法律知识" sub-title="查看更多"></u-section>
</u-col>
</u-row>
</view>
<view style="margin-left:2%;margin-right:2%;margin-top:20rpx;">
<u-cell-group>
<u-cell-item title="法律知识"></u-cell-item>
<u-cell-item title="法律知识"></u-cell-item>
</u-cell-group>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: 'XX法律咨询',
caseTypeList: [{
id: 1,
caseTypeName: "婚姻家庭",
caseTypeIcon: "/static/images/casetype/married&family.png"
},
{
id: 2,
caseTypeName: "刑事案件",
caseTypeIcon: "/static/images/casetype/married&family.png"
},
{
id: 3,
caseTypeName: "劳动工伤",
caseTypeIcon: "/static/images/casetype/married&family.png"
},
{
id: 4,
caseTypeName: "债权债务",
caseTypeIcon: "/static/images/casetype/married&family.png"
},
{
id: 5,
caseTypeName: "医疗赔偿",
caseTypeIcon: "/static/images/casetype/married&family.png"
},
{
id: 5,
caseTypeName: "医疗赔偿",
caseTypeIcon: "/static/images/casetype/married&family.png"
},
{
id: 5,
caseTypeName: "医疗赔偿",
caseTypeIcon: "/static/images/casetype/married&family.png"
},
{
id: 5,
caseTypeName: "更多",
caseTypeIcon: "/static/images/casetype/moreCaseType.png"
}
]
}
},
onLoad() {
},
methods: {
toChat() {
uni.navigateTo({
url: './chat/chat'
})
}
}
}
</script>
<style lang="scss" scoped>
$consultTitleRpx: 40rpx;
$consultTitleWeight: 700;
$consultSubTitleRpx: 25rpx;
$consultSubTitleColor: #888;
.consult {
font-size: $consultTitleRpx;
font-weight: $consultTitleWeight;
}
.consult-sub {
font-size: $consultSubTitleRpx;
color: $consultSubTitleColor;
margin-left: 0;
}
.card {
margin-left: 5%;
margin-right: 5%;
width: 90%;
box-shadow: 7rpx 8rpx 20rpx #eee;
}
.case-type {
text-align: center;
border: 1rpx solid transparent;
background-color: #f8f8fa;
padding-top: 30rpx;
padding-bottom: 30rpx;
border-radius: 10rpx;
color: #555;
width: 90%;
margin-top: 20rpx;
font-size: 25rpx;
}
</style>
四、推荐阅读
🥇入门和进阶小程序开发,不可错误的精彩内容🥇 :
- 《小程序开发必备功能的吐血整理【个人中心界面样式大全】》
- 《微信小程序 | 借ChatGPT之手重构社交聊天小程序》
- 《微信小程序 | 人脸识别的最终解决方案》
- 《微信小程序 |基于百度AI从零实现人脸识别小程序》
- 《吐血整理的几十款小程序登陆界面【附完整代码】》