前言
案例以智能导购为切入点,根据用户的语言描述查找接近的商品,以供客户选择。
用户通过自然语言描述自己的需求,AI解析转换将需求转换为客户系统商品的查询条件,将最终商品结果展示输出。
上面样例展示了两种显示方案:
1.使用官方的对话SDK组件;
- 使用官方API,由开发自己封装UI组件;
可以看到 官方SDK 带有品牌logo,且无法渲染定制化文案(如html样式文本);自定义UI 可以按自己需求处理最终显示。
企业的落地场景是多种多样且难以统一化的,所以最终应采用API方案自定义封装展示界面。
工作流·商品列表
工作流展示了,将用户的文本输入拆解为业务 api 的 json 输入,对查询到的商品信息进行html格式化并输出返回。

bash
开始:用户输入。
用户语言分析:通过ai分析用户输入,并分词分组。
构建where:对分词后的文本,拼装为知识库查询sql。
查询枚举id:到知识库中进行枚举匹配,将获取到的枚举id传递给下一步。
完善查询条件:将所有前置信息组装为业务api需要的请求参数。
商品列表查询:发起对业务api的查询。
结果判断:解析api结果。
是否有商品:分支,无商品直接返回;有商品继续下一步。
商品展示html:对api响应的商品信息转义为浏览器友好展示的html代码段。
结束:将最终结果输出。
知识库·枚举
业务中用到了资源库中的数据库,用于存储常用枚举关系,将枚举名称匹配为系统id。

机器人

UI 方案1.使用官方SDK
bot_id为机器人编号、token为用户令牌
官方文档:www.coze.cn/open/playgr...
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="./main.css" />
</head>
<body>
<!-- Install SDK -->
<script src="https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.8/libs/cn/index.js"></script>
</body>
<script>
// 内嵌Chat SDK
new CozeWebSDK.WebChatClient({
config: {
bot_id: '***',
},
componentProps: {
title: 'Coze',
},
auth: {
type: 'token',
token: '***',
onRefreshToken: async () => '',
}
});
</script>
</html>
UI 方案2.API自定义封装
bot_id为机器人编号、token为用户令牌
官方文档:www.coze.cn/open/docs/d...
程序后端
go
package main
import (
"errors"
"fmt"
"github.com/coze-dev/coze-go"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
"io"
"net/http"
"time"
)
type Result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
type CreateChatsReq struct {
UserId string `json:"user_id" form:"user_id"` // 用户id
ConversationId string `json:"conversation_id" form:"conversation_id"` // 对话id
Message string `json:"message"` // 消息内容
}
type MessageListReq struct {
ConversationId string `json:"conversation_id" form:"conversation_id"` // 对话id
}
func NewCli(token string) coze.CozeAPI {
// 通过个人访问令牌或oauth获取access_token。
authCli := coze.NewTokenAuth(token)
// 2. 用自定义基URL初始化
cozeAPIBase := "https://api.coze.cn" // 默认值 https://api.coze.com(无法请求)
// 3. 用自定义基URL初始化
customClient := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
cozeCli := coze.NewCozeAPI(authCli,
coze.WithBaseURL(cozeAPIBase),
coze.WithHttpClient(customClient),
)
return cozeCli
}
func main() {
token := "***"
botID := "***" // 智能体机器人ID。 进入智能体的开发页面,开发页面 URL 中 bot 参数后的数字就是智能体ID。例如https://www.coze.cn/space/341****/bot/73428668*****,bot_id 为73428668*****。
cozeCli := NewCli(token)
//userID := "U1" // 用户ID
r := gin.New()
r.Delims("[[", "]]")
r.LoadHTMLGlob("coze_model/web/*.html")
// 初始页
r.GET("sdk_api_ui", func(c *gin.Context) {
c.HTML(200, "api_ui.html", map[string]string{
"bot_id": botID,
"token": token,
})
})
// 获取机器人信息
r.GET("api/bot_info", func(c *gin.Context) {
resp, err := cozeCli.Bots.Retrieve(c, &coze.RetrieveBotsReq{
BotID: botID,
})
res := &Result{}
if err != nil {
res.Code = 1
res.Msg = err.Error()
} else {
res.Data = resp.Bot
}
c.JSON(http.StatusCreated, res)
})
// 聊天
r.POST("api/chat", func(c *gin.Context) {
r2 := &CreateChatsReq{}
if err := c.BindJSON(r2); err != nil {
c.JSON(http.StatusCreated, &Result{Code: 1, Msg: err.Error()})
return
}
if r2.UserId == "" {
c.JSON(http.StatusCreated, &Result{Code: 1, Msg: "未指定用户"})
return
}
if r2.Message == "" {
c.JSON(http.StatusCreated, &Result{Code: 1, Msg: "空消息"})
return
}
// 发起对话
req := &coze.CreateChatsReq{
BotID: botID,
UserID: r2.UserId,
Messages: []*coze.Message{
coze.BuildUserQuestionText(r2.Message, nil),
},
ConversationID: r2.ConversationId,
}
resp, err := cozeCli.Chat.Stream(c, req)
if err != nil {
c.JSON(http.StatusCreated, &Result{Code: 2, Msg: "启动聊天错误: " + err.Error()})
return
}
defer resp.Close()
// 响应流
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("Access-Control-Allow-Origin", "*")
c.Stream(func(w io.Writer) bool {
event, err := resp.Recv()
if errors.Is(err, io.EOF) {
w.Write([]byte("\n"))
return false
}
if err != nil {
w.Write([]byte("[error " + err.Error() + "]"))
return false
}
if event.Event == coze.ChatEventConversationChatCreated {
source, _ := jsoniter.MarshalToString(event.Chat) // conversation_id 需要客户端保存,以便后续持续对话。
w.Write([]byte(fmt.Sprintf("event: header\ndata: {\"conversation_id\":\"%s\", \"source\":%s}\n\n", event.Chat.ConversationID, source)))
}
if event.Event == coze.ChatEventConversationMessageDelta {
w.Write([]byte("event: body\ndata: " + event.Message.Content + "\n\n"))
} else if event.Event == coze.ChatEventConversationChatCompleted {
source, _ := jsoniter.MarshalToString(event.Chat)
w.Write([]byte(fmt.Sprintf("event: footer\ndata: {\"token_usage\":%v, \"source\":%s}\n\n", event.Chat.Usage.TokenCount, source)))
//} else {
// w.Write([]byte("\n"))
}
return true
})
})
// 消息列表
r.GET("api/message/list", func(c *gin.Context) {
r2 := &MessageListReq{}
if err := c.Bind(r2); err != nil {
c.JSON(http.StatusCreated, &Result{Code: 1, Msg: err.Error()})
return
}
if r2.ConversationId == "" {
c.JSON(http.StatusCreated, &Result{Code: 1, Msg: "请输入会话标识"})
return
}
order := "asc"
resp, err := cozeCli.Conversations.Messages.List(c, &coze.ListConversationsMessagesReq{
BotID: &botID,
ConversationID: r2.ConversationId,
Limit: 20,
Order: &order,
})
if err != nil {
c.JSON(http.StatusCreated, &Result{Code: 2, Msg: "启动聊天错误: " + err.Error()})
return
}
items := resp.Items()
list := []map[string]string{}
for _, item := range items {
list = append(list, map[string]string{
"content": item.Content,
"sender": item.Role.String(),
"created_at": time.Unix(item.CreatedAt, 0).Format(time.DateTime),
"updated_at": time.Unix(item.UpdatedAt, 0).Format(time.DateTime),
})
}
//b, _ := jsoniter.Marshal(&Result{Code: 0, Data: list})
c.JSON(http.StatusCreated, &Result{Code: 0, Data: list})
})
r.Run(":8081")
}
程序前端
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<title>api ui</title>
<style>
.message-container {
height: 400px;
padding: 20px;
overflow-y: auto;
border-bottom: 1px solid #eee;
margin-bottom: 15px;
}
.message-bubble {
max-width: 70%;
padding: 12px 15px;
margin: 8px 0;
border-radius: 4px;
word-break: break-word;
}
.message-bubble.user {
background-color: #ecf5ff;
color: #409eff;
margin-left: 20%;
}
.message-bubble.assistant {
background-color: #f5f7fa;
color: #606266;
margin-right: 20%;
}
</style>
</head>
<body>
<div id="app">
<div>
<el-row>
<el-col :span="6">
<el-descriptions title="机器人信息" v-if="bot_info" column="1">
<el-descriptions-item label="头像"><img :src="bot_info.icon_url" alt="头像" width="100px"></el-descriptions-item>
<el-descriptions-item label="名称">{{bot_info.name}}</el-descriptions-item>
<el-descriptions-item label="描述">{{bot_info.description}}</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
会话id<el-input v-model="conversation_id" placeholder="空" style="width: 200px" disabled></el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="6">
用户<el-input v-model="form_user_id" placeholder="标识" style="width: 100px"></el-input>
<el-button type="primary" @click="openDialog">新对话</el-button>
<el-button @click="getMessageList">历史对话</el-button>
</el-col>
</el-row>
<!-- 对话框组件 -->
<el-dialog :title="bot_info.name" :visible.sync="dialog_visible" width="600px" :before-close="handleClose" :append-to-body="true">
<div class="message-container">
<div v-for="(msg, index) in messages" :key="index" :class="['message-bubble', msg.sender === 'user' ? 'user' : 'assistant']">
<div v-html="msg.content"></div>
</div>
</div>
<el-row slot="footer">
<el-input type="textarea" v-model="input_msg" :rows="2" placeholder="请输入消息" @keyup.enter="sendMsg"></el-input>
<el-button type="text" size="small" @click="sendMsg" style="float: right">发送</el-button>
</el-row>
</el-dialog>
</div>
</div>
</body>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://lf-cdn.coze.cn/obj/unpkg/flow-platform/chat-app-sdk/1.2.0-beta.6/libs/cn/index.js"></script>
<script>
new Vue({
el: '#app',
data: function() {
return {
form_user_id: "U1",
user_id:"U1",
dialog_visible: false,
input_msg: '',
messages: [],
bot_info:{},
conversation_id: ''
}
},
mounted() {
this.get_bot_info()
},
methods: {
openDialog() {
// if (this.user_id !== this.form_user_id){ // 更换了用户,消息清空
this.user_id = this.form_user_id
this.messages = []
this.conversation_id = ''
// }
this.dialog_visible = true
this.input_msg = '' // 清空输入框
},
handleClose(done) {
this.$confirm('确认关闭对话?').then(() => {
done()
this.dialog_visible = false
}).catch(() => {})
},
// 发送消息
sendMsg() {
if (!this.input_msg.trim()) return
// 添加用户消息
this.messages.push({
sender: 'user',
content: this.input_msg
})
this.postStream('/api/chat', {user_id:this.user_id, message: this.input_msg, conversation_id: this.conversation_id}, res=>{
this.messages.push({
sender: 'assistant',
content: `已收到消息: ${this.input_msg}`
})
})
this.input_msg = '' // 清空输入框
this.$refs.dialog.$el.scrollTop = this.$refs.dialog.$el.scrollHeight // 自动滚动到底部
},
// 获取消息列表
getMessageList(){
this.$prompt('请输入会话id', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
}).then(({ value }) => {
this.get('/api/message/list',{user_id:this.user_id, conversation_id: value},res=>{
if(res.code!= 0){
this.$message.error("获取消息列表失败:"+res.msg)
return
}
this.messages = res.data
this.conversation_id = value
this.dialog_visible = true
})
}).catch(() => {});
},
// 获取机器人信息
get_bot_info(){
this.get('/api/bot_info',{},res=>{
if(res.code != 0){
this.$message.error("机器人异常:"+res.msg)
return
}
this.bot_info = res.data
})
},
get(path, data, success){
// let url = this.baseUrl+path
this.ajax('get', path, data, {},success)
},
post(path, data, success){
// let url = this.baseUrl+path
this.ajax('post', path, data, {'Content-Type': 'application/json'},success)
},
ajax: function (method, url, data, header, callback, async=true, dataType='json') {
$.ajax({
'url': url,
'data': data,
'type': method,
'headers': header,
'dataType': dataType,
'async': async,
'success': res => {
return callback(res)
},
'error': res =>{
let temp = {}
if (res.status == 0){
temp.code = -1
temp.msg = res.statusText
}else if(Object.keys(res.responseJSON).length > 0){
temp = res.responseJSON
}else {
temp.code = res.status
temp.msg = res.status + " " + res.statusText
}
return callback(temp)
}
});
},
// 处理Stream数据
async postStream(path, data, success) {
try {
const response = await fetch(path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream' // 明确声明接受流式响应
},
body: JSON.stringify(data)
});
// 检查响应状态
if (!response.ok) throw new Error(`HTTP错误!状态: ${response.status}`);
// 获取可读流
const reader = response.body.getReader(); // 获取流读取器
const decoder = new TextDecoder('utf-8');// 用于解码二进制数据
const item = {
sender: 'assistant',
content: ''
}
this.messages.push(item)
// 逐块读取数据
let eventType = ''
while (true) {
const { value, done } = await reader.read();
if (done) break; // 完成
// 解码二进制数据
const buffer = decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
lines.forEach(line => {
let body = ''
if (line.startsWith('event:')){ // 解析事件类型
eventType = line.slice(6).trim()
return
}else if (line.startsWith('data:')){ // 数据
body = line.slice(5).trim()
}else{
body = line
}
if (body == ""){
return;
}
switch (eventType) {
case 'body':
item.content += body
break;
case 'header':
let tmp = JSON.parse(body)
if (this.conversation_id === '' && tmp.conversation_id){
this.conversation_id = tmp.conversation_id
}
console.log("header", body)
break;
case 'footer':
console.log("footer", body)
break;
case '': // 未到流之前的消息
let other = JSON.parse(body)
if (other.code != 0){
this.$message.error(other.msg)
return
}
break;
default:
console.log('未知事件类型:', eventType, body);
}
return;
})
}
} catch (error) {
console.error('Stream 异常:', error);
}
}
}
})
</script>
</html>