背景
最近写了一篇文章突然上了热门,让我感到挺意外。针对那个46万行的超级系统,文章很多技术细节被我轻描淡写了,其实只是个人,系统治理的经验分享。
前面提到了,系统有几百个菜单,通过肌肉记忆,是很难记住的,如果你对业务不了解,找菜单也会显得笨手笨脚,因为下拉框的过滤需要精准匹配。今天借助于扣子API来实现找菜单功能,输入一句话:帮我打开订单列表,直接理解语义,打开订单列表页面。
这篇文章只是突发奇想,能否落地值得商榷,望大侠原谅。
方案
常规实现
使用vite+vue3简单搭建一个页面,在顶部增加搜索功能,把菜单数据组装成下拉数组,开启下拉组件的过滤属性。
这种方式有一个弊端,当菜单很多时,如果你输入的名称没有精准包含进去,就无法过滤出来,比如:订单列表写成订单页面,数组就会变成空,因为它其实通过indexOf来过滤,当菜单有几百个时,只能凭记忆来搜索名称。
右侧下拉框是常规的数据过滤,左侧下拉框是通过remote调用扣子API实现。
扣子实现
在扣子平台创建一个Bot,添加知识库,发布,最后通过Bot API来调用获取菜单路径,从而实现菜单跳转。
API调用
1. 打开扣子平台。
2. 创建Bot
3. 进入扣子编排页面。
4. 添加文本知识库。
- 点击【知识】下面的文本右侧的加号。
- 在选择知识库弹窗里面,点击【创建知识库】。
- 选择文本格式、导入类型选择自定义。
- 填写菜单名称和菜单路径
名称:工作台
路径:/workplace
名称:数据看板
路径:/dashboard
名称:用户列表
路径:/userList
名称:字典列表
路径:/dictList
名称:图表1
路径:/chart1
名称:图表2
路径:/chart2
名称:部门列表
路径:/deptList
名称:待办列表
路径:/todoList
名称:库存列表
路径:/stockList
名称:订单列表
路径:/orderList
名称:城市列表
路径:/cityList
上面为Demo数据,其实没有固定格式,你也可以用表格、txt等各种格式的数据,只要能达到最终目的即可。
- 最终应该是这样的:
5. 添加Bot人设和回复逻辑
你是一名数据查找助手,根据用户输入的名称,找到对应的路径,比如:
1. 打开用户管理,返回:/userList。
2. 用户xxx管理,返回:/userList。
3. 我要打开用户管理菜单,返回:/userList。
4. 我要找用户管理页面,返回:/userList。
5. 打开xxx页面、找到xxx页面、打开xxx菜单、找到xxx菜单等,其实就是为了查找名称为xxx的路径。比如:打开用户页面,就是查找名称等于用户列表或者用户管理对应的路径,/userList。
6. xxx菜单,其实就是查找名称为xxx的路径,比如:看板菜单,对应的就是:/dashboard。
# 要求
1. 如果没有找到,直接返回:暂无数据。
2. 如果找到多个,则直接返回第一个。
3. 回复的时候,请不要说:xxx对应的路径是:/xxx。 请直接回复确定的答案,比如:/userList。
大家可以自己控制机器人的回复逻辑。
6. 在右侧进行调试
- 比如:输入打开订单页面
- 语义很模糊
- 当查找到多条数据时,返回第一条
7. 点击右上角的发布
8. 发布时,勾选最下面的Bot API和Web SDK。
Bot as API 需要点击配置,创建一个【个人访问令牌】,生成的密钥要保存,因为密钥无法二次查看。
9. 发布成功后,会进入到下面这个界面
10. API 集成
设置下拉框为远程搜索,根据文档,先调用创建对话接口,再轮询调用详情接口。
- 局部代码:
<el-select
v-model="search"
placeholder="请选择菜单"
filterable
remote
:remote-method="fetchData"
:loading="loading"
style="width: 240px"
@change="handleChange"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.name"
:value="item.link"
/>
</el-select>
<script setup>
import { ref } from "vue";
const search = ref("");
const options = ref([]);
const T = ref(null);
const fetchData = (query) => {
if (!query) return;
if (T.value) {
clearTimeout(T.value);
T.value = setTimeout(() => {
// TODO
}, 500);
} else {
T.value = setTimeout(() => {
// TODO
}, 500);
}
};
</script>
当输入内容时,加一个防抖函数,避免接口调用频繁。接着调用对应接口。
- 调用创建对话接口
const chat = (query) => {
let url = "https://api.coze.cn/v3/chat";
loading.value = true;
fetch(url, {
headers: {
Authorization:
"Bearer xxx",
"Content-Type": "application/json",
},
method: "POST",
mode: "cors",
body: JSON.stringify({
bot_id: "7394754487971938354",
user_id: "1024",
stream: false,
auto_save_history: true,
additional_messages: [
{
role: "user",
content: query,
content_type: "text",
},
],
}),
})
.then((response) => {
return response.json();
})
.then((res) => {
if (res.data.status === "in_progress") {
setTimeout(() => {
chatDetail(res.data.conversation_id, res.data.id);
}, 1000);
}
}).catch(()=>{
loading.value = false;
});
};
注意:Bearer 后面需要添加个人令牌,上面有提到过。如果返回的status字段为in_progress,需要继续调用对话详情接口,拿到最终结果。另外调用详情接口的延迟时间不要太短,可能会有坑。
- 调用详情接口
const chatDetail = (conversation_id, chat_id) => {
let url =
"https://api.coze.cn/v3/chat/message/list?conversation_id=" +
conversation_id +
"&chat_id=" +
chat_id;
fetch(url, {
headers: {
Authorization:
"Bearer pat_435qwjg0WC2t4UuU4kZxiIeII0wI79inv1pgiomJ1qMiM9GWlzPPTKRG7QorfS7o",
"Content-Type": "application/json",
},
method: "POST",
mode: "cors",
})
.then((response) => {
return response.json();
})
.then((res) => {
if (res.data) {
console.log(res.data);
const item = res.data.filter((item) => item.type === "answer")?.[0];
if (item) {
// this.searchResult = item.content;
if (item.content.trim() != "暂无数据") {
const path = item.content.trim();
options.value = [{ name: path, link: path }];
} else {
options.value = [];
}
loading.value = false;
} else {
setTimeout(() => {
chatDetail(conversation_id, chat_id);
}, 1000);
}
} else {
setTimeout(() => {
chatDetail(conversation_id, chat_id);
}, 1000);
}
});
};
注意:接收会话ID和chatId作为参数,对结果进行过滤,如果发现有type='answer'说明已经开始拿到返回结果了,如果没有answer,继续轮询调用,同样添加延迟时间。 这里面我发现有一个问题,有的情况下,即使拿到answer也不是最终结果,因为它其实是流式响应,我们其实并没有开启stream,所以建议把延迟时间放大,尽量拿到最终结果。
拿到以后,保存到options中,作为下拉框的数据源。
- 测试验证
点击会直接打开工作台页面。
用自然的交流方式,同样可以找到订单列表页面,点击实现跳转。
SDK 接入
除了API调用方式,我们也可以选择SDK接入,但是SDK接入,一般的场景可能就是机器人客服、在线问答等,对于找菜单可能作用不是很明显,因为他返回的路径你没办法直接跳转,只能手动复制,然后输入到url后,才能打开对应页面。
- 在发布成功页面,点击
Web SDK的安装按钮。
- 把复制的代码放到
index.html的body中。
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script src="https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/0.1.0-beta.5/libs/cn/index.js"></script>
<script>
new CozeWebSDK.WebChatClient({
config: {
bot_id: "7394754487971938354",
},
componentProps: {
title: "Coze",
},
});
</script>
</body>
- 刷新页面
右下角会生成一个按钮,点击打开聊天窗口。
- 输入:打开工作台
它会返回:/workplace ,但是无法跟页面交互,需要手动复制才能打开,所以聊天窗口不适合当前业务场景。
总结
其实菜单搜索,就用常规的下拉过滤就足够了,今天只是突发奇想,用扣子API来尝试实现,打开思路,会有不一样的收获。尽管这样做,我感觉意义不大,但是经过这样的尝试,让我对扣子的使用有了进一步的了解,如果不尝试,可能我还是一知半解。
之前看到前端大佬 前端小付 有一篇文章也是关于语义理解的,写的很不错,他用的是 typechat做的,因为我没有openai账号,所以,我就用扣子的知识库插件实现了。
感谢各位客官倾听,我是河畔一角,一名普通的前端。