前言
最近的一个科研项目,有一个非常离谱的需求,领导希望用户的问题,即使没有关键词,但意思相关,仍然搜索到相关的数据,并生成思维导图。
代码
- 需要引入两个依赖
import 'jsmind/style/jsmind.css';
import jsMind from 'jsmind';
- 通过prompt控制大模型对用户语义分析关键词,并控制输出格式 这一步需要大模型有足够的格式控制能力,否则需要手动解析大模型返回的json数据
const keywordPrompt = `${keyword.value},请根据上述问题继续语义分析,提取出多个关键词进行数据检索,只回复我数组code即可,例如:["伺服驱动器","伺服电机"]`
const keywordResponse = await fetch("http://127.0.0.1:11434/api/generate", {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "*/*",
"Host": "127.0.0.1:11434",
"Connection": "keep-alive"
},
body: JSON.stringify({
"model": "qwen2.5:14b",
"prompt": keywordPrompt,
"stream": false
})
});
- 将上一步分析的关键词数组用于搜索数据库
for (let i = 0; i < keywords.length; i++) {
const searchResponse = await axios.get(`/search?keyword=${keywords[i]}`);
console.log(searchResponse.data)
searchResults[keywords[i]] = searchResponse.data || [];
}
- 将数据库返回的数据转换为jsmind接收的数据格式,并对应上一步分析的关键词-》多条数据库返回的数据
const mind = {
meta: {
name: '分析结果',
author: 'system',
version: '1.0'
},
format: 'node_tree',
data: {
id: 'root',
topic: keyword.value, // 根节点显示用户输入的关键词
children: [] // 子节点数组
}
}
最终效果演示
最后
这个实现思路或许比较粗糙,但确实也“实现”了甲方需求, 其实我也尝试了其他方法,比如:向量数据库检索和一些开源的本地知识库,但向量检索需要大量的数据,并且需要训练,而本地知识库的效果并不好,而大模型只需要prompt即可,所以最后还是选择了大模型。
但依然有一些不足的地方,尽管qwen2.5-14b的模型的上下文已经很大,但如果系统的数据量日渐增多,也终有一天会达到上下文限制。
如果有更好的解决方案,欢迎交流。
附上完整的代码
<template>
<div class="ma-content-block lg:flex justify-between p-4">
<div class="background-container ma-content-block lg:flex justify-between p-4">
<div class="image-container">
<div class="loader-container">
<div class="loader">
<div class="cell d-0"></div>
<div class="cell d-1"></div>
<div class="cell d-2"></div>
<div class="cell d-1"></div>
<div class="cell d-2"></div>
<div class="cell d-2"></div>
<div class="cell d-3"></div>
<div class="cell d-3"></div>
<div class="cell d-4"></div>
</div>
</div>
<h1 class="text-center text-2xl font-bold text-gray-800 mt-4 p-ttext" style="font-size: 35px;">
<span ref="typewriterText"></span>
</h1>
<p class="p-ttext-p text-center text-lg font-bold text-gray-800 mt-4 " style="font-size: 15px; color: #666;">
运用先进的自然语言处理技术和深度学习模型,对设备问题进行精准的语义分析和多维度理解,实现高效的维修记录检索和智能关联,展示出思维导图,为您提供全面而深入的问题诊断和解决方案。
</p>
</div>
<div class="input-container">
<a-input v-model="keyword" type="text" placeholder="请输入关键词,例如:伺服驱动器出现问题" class="custom-input" />
</div>
<div class="mt-4">
<a-button type="primary" class="submit-button" @click="handleSearch">分析</a-button>
</div>
</div>
<a-modal v-model:visible="showResult" title="分析结果" :footer="null" width="80%">
<div class="result-container">
<div class="model-content">
<div v-if="isLoading" class="loading-indicator">
<a-spin :size="36" />
<p style="z-index: 15;">模型推理中...</p>
<div class="spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<div v-else>
<div id="jsmind_container" style="width: 100%; height: 76vh;"></div>
</div>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch, computed } from 'vue'
import axios from 'axios'
import 'jsmind/style/jsmind.css';
import jsMind from 'jsmind';
import { Message } from '@arco-design/web-vue';
const typewriterText = ref(null)
const texts = ['数据检索', '智能分析', '图形化展示']
let currentTextIndex = 0
let currentCharIndex = 0
const keyword = ref('')
const showResult = ref(false)
const isLoading = ref(false)
let jm = null
const typeText = () => {
if (!typewriterText.value) return
if (currentCharIndex < texts[currentTextIndex].length) {
typewriterText.value.textContent += texts[currentTextIndex][currentCharIndex]
currentCharIndex++
setTimeout(typeText, 100)
} else {
setTimeout(eraseText, 2000)
}
}
const eraseText = () => {
if (!typewriterText.value) return
if (currentCharIndex > 0) {
typewriterText.value.textContent = texts[currentTextIndex].substring(0, currentCharIndex - 1)
currentCharIndex--
setTimeout(eraseText, 50)
} else {
currentTextIndex = (currentTextIndex + 1) % texts.length
setTimeout(typeText, 500)
}
}
const initJsMind = (keywords, results) => {
const mind = {
meta: {
name: '分析结果',
author: 'system',
version: '1.0'
},
format: 'node_tree',
data: {
id: 'root',
topic: keyword.value,
children: []
}
}
keywords.forEach((kw, index) => {
const keywordNode = {
id: `keyword_${index}`,
topic: kw,
children: []
}
results[kw]?.forEach((result, rIndex) => {
const problemNode = {
id: `result_${index}_${rIndex}`,
topic: result.problem_description || '相关记录',
children: [{
id: `solution_${index}_${rIndex}`,
topic: `解决方法:${result.solution || '暂无解决方案'}`
}]
}
keywordNode.children.push(problemNode)
})
mind.data.children.push(keywordNode)
})
const options = {
container: 'jsmind_container',
theme: 'primary',
editable: false,
mode: 'full',
view: {
hmargin: 100,
vmargin: 50,
line_width: 2,
line_color: '#555'
}
}
// 清除旧的思维导图
const container = document.getElementById('jsmind_container')
if (container) {
container.innerHTML = ''
}
// 重新初始化
setTimeout(() => {
jm = new jsMind(options)
jm.show(mind)
}, 100)
}
const handleSearch = async () => {
try {
if (!keyword.value) {
Message.error('请输入需要分析的问题');
return;
}
showResult.value = true;
isLoading.value = true;
const keywordPrompt = `${keyword.value},请根据上述问题继续语义分析,提取出多个关键词进行数据检索,只回复我数组code即可,例如:["伺服驱动器","伺服电机"]`
const keywordResponse = await fetch("http://127.0.0.1:11434/api/generate", {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "*/*",
"Host": "127.0.0.1:11434",
"Connection": "keep-alive"
},
body: JSON.stringify({
"model": "qwen2.5:14b",
"prompt": keywordPrompt,
"stream": false
})
});
const keywordResult = await keywordResponse.json();
let keywords;
try {
keywords = JSON.parse(keywordResult.response);
if (!Array.isArray(keywords)) {
throw new Error('模型返回格式错误');
}
} catch (parseError) {
throw new Error('模型返回的数据格式有误,请重新输入更具体的问题描述');
}
const searchResults = {}
for (let i = 0; i < keywords.length; i++) {
const searchResponse = await axios.get(`/search?keyword=${keywords[i]}`);
console.log(searchResponse.data)
searchResults[keywords[i]] = searchResponse.data || [];
}
isLoading.value = false;
initJsMind(keywords, searchResults);
} catch (error) {
console.error('分析出错:', error);
isLoading.value = false;
showResult.value = false;
Message.error({
content: error.message || '分析过程出现错误,请稍后重试',
duration: 3000
});
}
}
onMounted(() => {
typeText()
})
</script>
<script>export default { name: 'maintenance:search' } </script>
<style lang="less" scoped>
.background-container {
width: 100%;
min-height: 90vh;
position: relative;
background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 2rem;
}
.image-container {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 400px;
}
.p-ttext {
position: fixed;
top: 100%;
left: 50%;
transform: translateX(-50%);
}
.p-ttext-p {
position: fixed;
top: 135%;
left: 50%;
width: 150%;
transform: translateX(-50%);
}
.loader-container {
position: relative;
top: 20%;
left: 50%;
transform: translateX(-25%);
}
.input-container {
position: absolute;
bottom: 36%;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 600px;
}
.custom-input {
width: 100%;
padding: 12px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
&:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
.submit-button {
position: absolute;
bottom: 29%;
left: 50%;
transform: translateX(-50%);
padding: 0 2rem;
height: 40px;
font-size: 16px;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
opacity: 0.9;
transform: translate(-50%, -2px);
}
}
.result-container {
height: 80vh;
overflow-y: auto;
}
.model-content {
margin-bottom: 20px;
padding: 15px;
background-color: #f0f8ff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
}
#jsmind_container {
width: 100%;
height: 500px;
}
</style>
<style scoped>
/* The loader container */
/* From Uiverse.io by alexruix */
.loader {
--cell-size: 52px;
--cell-spacing: 1px;
--cells: 3;
--total-size: calc(var(--cells) * (var(--cell-size) + 2 * var(--cell-spacing)));
display: flex;
flex-wrap: wrap;
width: var(--total-size);
height: var(--total-size);
}
.cell {
flex: 0 0 var(--cell-size);
margin: var(--cell-spacing);
background-color: transparent;
box-sizing: border-box;
border-radius: 4px;
animation: 1.5s ripple ease infinite;
}
.cell.d-1 {
animation-delay: 100ms;
}
.cell.d-2 {
animation-delay: 200ms;
}
.cell.d-3 {
animation-delay: 300ms;
}
.cell.d-4 {
animation-delay: 400ms;
}
.cell:nth-child(1) {
--cell-color: #00FF87;
}
.cell:nth-child(2) {
--cell-color: #0CFD95;
}
.cell:nth-child(3) {
--cell-color: #17FBA2;
}
.cell:nth-child(4) {
--cell-color: #23F9B2;
}
.cell:nth-child(5) {
--cell-color: #30F7C3;
}
.cell:nth-child(6) {
--cell-color: #3DF5D4;
}
.cell:nth-child(7) {
--cell-color: #45F4DE;
}
.cell:nth-child(8) {
--cell-color: #53F1F0;
}
.cell:nth-child(9) {
--cell-color: #60EFFF;
}
/*Animation*/
@keyframes ripple {
0% {
background-color: transparent;
}
30% {
background-color: var(--cell-color);
}
60% {
background-color: transparent;
}
100% {
background-color: transparent;
}
}
</style>
<style>
.spinner {
width: 70.4px;
height: 70.4px;
--clr: rgb(247, 197, 159);
--clr-alpha: rgb(247, 197, 159, .1);
animation: spinner 1.6s infinite ease;
transform-style: preserve-3d;
}
.spinner>div {
background-color: var(--clr-alpha);
height: 100%;
position: absolute;
width: 100%;
border: 3.5px solid var(--clr);
}
.spinner div:nth-of-type(1) {
transform: translateZ(-35.2px) rotateY(180deg);
}
.spinner div:nth-of-type(2) {
transform: rotateY(-270deg) translateX(50%);
transform-origin: top right;
}
.spinner div:nth-of-type(3) {
transform: rotateY(270deg) translateX(-50%);
transform-origin: center left;
}
.spinner div:nth-of-type(4) {
transform: rotateX(90deg) translateY(-50%);
transform-origin: top center;
}
.spinner div:nth-of-type(5) {
transform: rotateX(-90deg) translateY(50%);
transform-origin: bottom center;
}
.spinner div:nth-of-type(6) {
transform: translateZ(35.2px);
}
@keyframes spinner {
0% {
transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
}
50% {
transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
}
100% {
transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
}
}
</style>