zoce AI 平台应用demo

前言

案例以智能导购为切入点,根据用户的语言描述查找接近的商品,以供客户选择。

用户通过自然语言描述自己的需求,AI解析转换将需求转换为客户系统商品的查询条件,将最终商品结果展示输出。

上面样例展示了两种显示方案:

1.使用官方的对话SDK组件;

  1. 使用官方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>
相关推荐
MrSkye3 小时前
从零到一:我用AI对话写出了人生第一个弹幕游戏 | Prompt编程实战心得
前端·ai编程·trae
用户18881478528814 小时前
谷歌Gemini编程吊打全场,国产AI全歇菜。
ai编程
俞乾4 小时前
这三个 MCP 组合,让 Cursor 指哪打哪
ai编程·cursor·vibecoding
沐森4 小时前
langchain相关介绍
langchain·ai编程
量子位5 小时前
Scaling Law 首次在自动驾驶赛道被验证!小鹏汽车 CVPR 演讲详解:AI「吃」下 6 亿秒视频后,智能涌现
chatgpt·ai编程
量子位5 小时前
全方位实测首个 AI 原生浏览器!618 比价、写高考作文... 网友:再见 Chrome
人工智能·ai编程
加16 小时前
95%代码AI生成,是的你没听错...…
前端·ai编程
我姚学AI8 小时前
大厂程序员自研Flomo AI插件,1秒总结笔记,官方都没做到!
ai编程
Jaising6669 小时前
JetBrains AI 打零工(四)——维护 Junie Guidelines 与代码可追溯
ai编程·intellij idea
星尘洞见9 小时前
发现了吗?Claude Sonnet 4变成"夸夸机器-你说的对"!5招破解AI编程助手套路
ai编程·cursor