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>
相关推荐
野生的码农1 小时前
放过自己,降低预期,及时行乐
android·ai编程
程序员陆业聪1 小时前
裸奔的 AI 助手和装备齐全的 AI 助手,根本不是同一个东西
ai编程
南木元元6 小时前
别只会用 Cursor!它的提示词工程才是真正的大招
ai编程·cursor
對玛祷至昏6 小时前
Trae AI编程入门
ai编程
小徐敲java6 小时前
opencode配置本地模型
ai编程
序舟归桁7 小时前
OpenClaw 多智能体在编程领域的实践与挑战
ai编程
序舟归桁7 小时前
Harness Engineering:AI Agent 时代,工程师的新核心能力
ai编程
攻城狮_老李7 小时前
从零开始理解 Agent Skills:动手实践 —— 创建第一个 Skill
openai·agent·ai编程
甲维斯8 小时前
来看看GLM5.1到底升级了什么!
ai编程