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>
相关推荐
YoungHong19927 小时前
MiniMax-M2 全方位配置手册:覆盖 Claude Code, Cursor, Cline 等工具
ai编程
人工智能训练7 小时前
如何在 Ubuntu 22.04 中安装 Docker 引擎和 Linux 版 Docker Desktop 桌面软件
linux·运维·服务器·数据库·ubuntu·docker·ai编程
数据智能老司机11 小时前
Spring AI 实战——提交用于生成的提示词
spring·llm·ai编程
数据智能老司机12 小时前
Spring AI 实战——评估生成结果
spring·llm·ai编程
该用户已不存在13 小时前
免费的 Vibe Coding 助手?你想要的Gemini CLI 都有
人工智能·后端·ai编程
一只柠檬新15 小时前
当AI开始读源码,调Bug这件事彻底变了
android·人工智能·ai编程
用户40993225021216 小时前
Vue 3中watch侦听器的正确使用姿势你掌握了吗?深度监听、与watchEffect的差异及常见报错解析
前端·ai编程·trae
yaocheng的ai分身18 小时前
【转载】我如何用Superpowers MCP强制Claude Code在编码前进行规划
ai编程·claude
重铸码农荣光18 小时前
从逐行编码到「氛围编程」:Trae 带你进入 AI 编程新纪元
ai编程·trae·vibecoding
Juchecar19 小时前
利用AI辅助"代码考古“操作指引
人工智能·ai编程