分享获得最佳技术奖的 coze 应用智能分析助手的技术实现

2,183 阅读12分钟

前言

coze出来后,我就一直在关注,做了一些小应用,参加了一些活动,得了一些小奖,算是积累了一些使用心得。

有天在某个群里看一个官方发布的活动,奖励挺吸引人的,并且当时我也有一些想法,所以就报着试试的心态参加了。

image.png

image.png

那段时间,每天下班都搞的很晚,有两天3点多睡的,第二天还继续上班,不过功夫不负有心人,最终得奖了。

image.png

体验地址:智能分析助手

得奖后有不少人私聊我,希望能把使用的技术分享一下,所以下面就和大家分享一下智能分析助手技术实现。

阅读本文需要有coze基础,建议大家可以先看下官方文档入一下门。

功能演示

excel数据分析

  1. 先准备一个excel

image.png

  1. 到智能分析助手里上传excel

image.png

  1. 上传成功后,大模型会根据你的数据,为你生成三条推荐问题

image.png

  1. 点击其中一个问题,大模型会先回答你的问题

image.png

  1. 然后根据你的问题答案,智能生成图表和数据解析。

image.png

  1. 根据上一个问题,再为你推荐三个问题。

image.png

  1. 如果你想自己问问题,也是可以的

image.png

image.png

不知道大家有没有发现,我准备的excel表里数据是没有利润字段的,但是这里给动态计算出来了,拿销售额减去成本。

我在excel里写了一个公式试了一下,计算结果都是对的。

image.png

  1. 在浏览器中查看大图

image.png

这里的图表渲染成svg格式,所以无论放大还是缩小不会模糊。

数据库数据分析

  1. 准备数据库数据,这里我使用的是从云顶之弈大数据网站爬下来的棋子数据

image.png

  1. 输入数据库信息,连接数据库

image.png

image.png

  1. 连接数据库成功后和excel分析流程一样会生成三个推荐问题

image.png

  1. 如果不想使用推荐问题,也可以输入自己的问题

image.png

image.png

为什么做智能分析

为什么要做智能分析呢,因为我们公司就是做分析的,我对分析的流程比较熟悉。

不过我们公司做的分析还是靠人工拖字段的方式生成图表,对不熟悉系统的人来说,还是有一点上手成本的。

我做的智能分析助手,更偏向于大众使用,每个人只要上传excel,通过问问题的方式,就能得到自己的答案,基本没有啥上手成本。

整体实现思路

第一版

  1. 写一个插件根据用户上传的excel,解析出excel里的内容,转换为二维数组格式。
  2. 把excel数据和用户问题传给大模型,让大模型根据问题返回数据,并使用大模型解析问题中维度字段和指标字段并返回。
  3. 根据用户问题以及上一步返回的数据,让大模型生成答案返回给用户
  4. 根据维度字段和指标字段以及数据,使用代码组装成图表渲染插件的参数渲染图表。
  5. 把excel数据传给大模型让大模型生成三个推荐问题

这是我最开始的实现思路,但是在实战中发现2个问题没办法解决,所以放弃了这个方案

  1. 如果excel数据量太大,把整个excel数据传给大模型,会超出大模型的字数限制。
  2. 大模型不适合做计算,假如我要算汇总数据,大模型可以把式子准确的列出来,但是最后计算结果是错的,试了好几个大模型计算结果都是错的。

image.png

image.png

第二版

  1. 写一个插件根据用户上传的excel,解析出excel里的内容,转换为二维数组格式。
  2. 根据二维数组动态生成表,第一行是表字段,后面行都是表数据。
  3. 根据用户问题和表字段,让大模型生成查询sql。
  4. 执行查询sql,查询数据
  5. 根据用户问题和查询出来的数据,让大模型生成答案返回给用户。
  6. 根据用户问题和查询出来的数据,生成echarts图表渲染参数。(这个真是惊喜了,没想到大模型这么强,生成的参数准确率很高,可以直接使用。本来我都打算自己拼接参数的。)
  7. 根据表字段让大模型为用户生成三个推荐问题

技术实现

提示词

提示词很简单,就是根据用户不同输入调用不同的工作流

image.png

数据库

因为上传完excel后,还要支持持续对话,所以需要定义一个数据库存放配置信息。

image.png

image.png

工作流

创建了三个工作流,这三个工作流是这个应用的核心。

image.png

解析excel数据,生成推荐问题

工作流整体设计

image.png

开始节点

这里有不少人疑惑,不知道怎么获取用户上传的excel,其实很简单。coze调用工作流的时候,会把上传的excel连接,传给工作流。

只需要在开始节点定义一个参数接连接就行了

下载excel并把数据转换为数据库表

这一步本来打算使用coze里的插件实现的,虽然coze插件功能很强大,基本可以实现需求。可惜coze里的数据库不支持动态创建表,我只能自己写一个后端服务,然后在工作流里调用自己的服务。

这里的服务框架使用的是node中的midway框架,数据库orm使用的typeorm。

创建一个接口,通过excel url下载excel,读取excel数据转换为二维数组,校验数据,动态创建表,插入数据。

image.png

校验数据

image.png

调用typeorm里的方法动态创建表,中间有段数据校验代码省略没截出来

image.png

往新建的表里插入数据,最后把刚创建的表名和字段列表返回出去

image.png

下载excel方法实现

/**
 * 下载Excel文件
 *
 * @param downloadPath 下载路径
 * @returns 返回包含Excel文件内容的Buffer对象,若下载失败则返回Promise的rejected状态
 */
export function downloadExcel(downloadPath: string) {
  return new Promise((resolve, reject) => {
    let dataBuffer = Buffer.from([]);
    https
      .get(downloadPath, response => {
        response.on('data', chunk => {
          dataBuffer = Buffer.concat([dataBuffer, chunk]);
        });

        response.on('end', () => {
          resolve(dataBuffer);
        });
      })
      .on('error', err => {
        reject(err);
      });
  });
}

把excel数据转换为二维数组方式实现,使用xlsx库解析excel数据。

/**
 * 将 Excel 表格数据转换为 JSON 格式
 *
 * @param buffer Excel 文件的二进制数据
 * @param type Excel 文件类型,可选值为 'csv' 或其他 xlsx 支持的文件类型
 * @returns 转换后的 JSON 数据,以二维数组形式返回
 */
export function excelToJson(buffer: any, type: string): string[][] {
  let workbook;
  if (type === 'csv') {
    const decodedData = iconv.decode(buffer, 'utf8');
    workbook = xlsx.read(decodedData, { type: 'string' });
  } else {
    workbook = xlsx.read(buffer, { type: 'buffer' });
  }

  // 获取 Excel 第一个工作表的名称
  const sheetName = workbook.SheetNames[0];

  // 通过工作表名称获取数据
  const sheet = workbook.Sheets[sheetName];

  if (sheet['!merges']) {
    return;
  }

  // 将 Excel 表格数据转换为 JSON 格式
  const excelData = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any;

  return excelData;
}

回到coze工作流中,在开始节点后添加一个代码节点,调用刚写好的接口

image.png

image.png

往coze数据库中插入配置信息

后面添加一个选择器,判断excel是否解析成功,成功后删除上一次的配置,把当前表名插入进去。如果失败,则输出失败消息。

image.png

生成3个推荐问题

接着使用大模型根据表字段生成三个推荐问题,因为返回的表字段是数组,所以先用代码处理成字符串。

image.png

大模型的系统提示词

image.png

在用一个节点,把大模型返回的数据格式化一下。

image.png

结束节点配置

最后处理错误信息,以及结束节点配置,把错误信息和三个推荐问题变量返回出去

image.png

使用卡片展示结果

创建卡片,拖三个按钮和一个文字节点进画布,给三个按钮分别绑定发送消息动作,消息内容就是显示的文字,文字节点绑定消息变量。

image.png

image.png

结束节点绑定卡片

image.png

image.png

配置快捷指令

image.png

image.png

选择刚才新建的工作流,自动生成模板

image.png

最后效果

image.png

连接数据库,生成推荐问题

总览

image.png

解析数据库连接信息

使用大模型把用户输出的数据库连接信息转换为json数据

image.png

image.png

校验输入的连接信息,是否有些字段为空

image.png

image.png

写一个coze插件,测试连接信息是否准确,并查询当前表有哪些字段。

image.png

使用mysql2库

image.png

import { Args } from '@/runtime';
import { Input, Output } from "@/typings/check_database_link/check_database_link";
import mysql, { createConnection, RowDataPacket } from 'mysql2/promise'

interface Table extends RowDataPacket {
  tableName: string;
  tableComment: string;
}

/**
  * Each file needs to export a function named `handler`. This function is the entrance to the Tool.
  * @param {Object} args.input - input parameters, you can get test input value by input.xxx.
  * @param {Object} args.logger - logger instance used to print logs, injected by runtime
  * @returns {*} The return data of the function, which should match the declared output parameters.
  * 
  * Remember to fill in input/output in Metadata, it helps LLM to recognize and use tool.
  */
export async function handler({ input }: Args<Input>): Promise<Output> {
  try {
    const connection = await createConnection(input);

    // 查询数据库中有哪些表
    const [results] = await connection.query<Table[]>(
      `SELECT
    table_name AS tableName, 
    table_comment as tableComment
FROM
    information_schema.tables
WHERE
    table_schema = '${input.database}';`
    );

    const table = results.find(o => o.tableName === input.table);

    if (!table) {
      return {
        success: false,
        msg: `当前表(${input.table})不存在,请检查是否输入错误。`,
      }
    }


    const tableInfo = await getTableInfo(connection, input.table, table.tableComment);

    connection.destroy();
    return {
      success: true,
      msg: '测试成功',
      tableComment: tableInfo.tableComment,
      tableName: tableInfo.tableName,
      fields: tableInfo.fields,
    }
  } catch (error) {
    return {
      success: false,
      msg: error.message,
    }
  }
};

async function getTableInfo(connection: mysql.Connection, tableName: string, tableComment: string) {
  const [results] = await connection.query(
    `SELECT COLUMN_NAME as name, COLUMN_COMMENT as comment
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_NAME = '${tableName}'`
  );

  return {
    tableName,
    tableComment,
    fields: results as any,
  };
}
测试成功后,把用户输入的数据库信息存入数据库

image.png

根据表名和表字段让大模型生成推荐问题

image.png

大模型的系统提示词

image.png

处理错误消息和配置结束节点

image.png

绑定卡片

这一步和excel一样,省略了。

快捷指令

image.png

把变量插入到指令内容中

image.png

最终效果

image.png

image.png

image.png

回答用户问题,智能生成图表

工作流总览

image.png

查询数据库,获取当前配置信息

image.png

根据不同的配置类型,返回数据库配置信息

如果类型是excel,数据库信息就是我服务器里的数据库信息,写死的。

如果类型是database,数据库信息就是用户自己输入的数据库信息。

数据库节点有个地方要注意一下,查询出来的字段需要在输出那里配置一下,不然后面取不到值。

添加选择器,如果没查询到配置信息,提示用户上传excel或连接数据库

image.png

写一个插件,根据数据库连接信息获取表结构

image.png

image.png

插件代码

import { Args } from '@/runtime';
import { Input, Output } from "@/typings/database_table_info/database_table_info";

import mysql, {RowDataPacket} from 'mysql2/promise';


interface Table extends RowDataPacket {
  tableName: string;
  tableComment: string;
}

/**
  * Each file needs to export a function named `handler`. This function is the entrance to the Tool.
  * @param {Object} args.input - input parameters, you can get test input value by input.xxx.
  * @param {Object} args.logger - logger instance used to print logs, injected by runtime
  * @returns {*} The return data of the function, which should match the declared output parameters.
  * 
  * Remember to fill in input/output in Metadata, it helps LLM to recognize and use tool.
  */
export async function handler({ input }: Args<Input>): Promise<Output> {

  // // Create the connection to database
  const connection = await mysql.createConnection({
    host: input.host,
    user: input.user,
    database: input.database,
    password: input.password,
    port: input.port,
  });


  // 查询数据库中有哪些表
  const [results] = await connection.query<Table[]>(
    `SELECT
    table_name AS tableName, 
    table_comment as tableComment
FROM
    information_schema.tables
WHERE
    table_schema = '${input.database}';`
  );

  const tables = await Promise.all(results.map(item => getTableInfo(connection, item.tableName, item.tableComment)));

  connection.destroy();

  return {
    data: {
      tables,
    } as any
  }
};

async function getTableInfo(connection: mysql.Connection, tableName: string, tableComment: string) {
  const [results] = await connection.query(
    `SELECT COLUMN_NAME as name, COLUMN_COMMENT as comment
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_NAME = '${tableName}'`
  );

  return {
    tableName,
    tableComment,
    fields: results as any,
  };
}
根据数据库表结构和用户输入的问题,使用大模型生成sql。

先使用代码节点格式化一个表结构信息,然后传给大模型。

image.png

大模型提示词

image.png

这里限制只能生成查询语句,防止误删数据。

写一个coze插件,动态执行sql获取查询结果

image.png

image.png

插件代码

import { Args } from '@/runtime';
import { Input, Output } from "@/typings/sql_execute/sql_execute";
import mysql from 'mysql2/promise'

/**
  * Each file needs to export a function named `handler`. This function is the entrance to the Tool.
  * @param {Object} args.input - input parameters, you can get test input value by input.xxx.
  * @param {Object} args.logger - logger instance used to print logs, injected by runtime
  * @returns {*} The return data of the function, which should match the declared output parameters.
  * 
  * Remember to fill in input/output in Metadata, it helps LLM to recognize and use tool.
  */
export async function handler({ input, logger }: Args<Input>): Promise<Output> {

  if (!input.sql) {
    return {
      data: '' as any,
      success: false,
      message: 'sql不能为空',
    }
  }

  const connection = await mysql.createConnection({
    host: input.host,
    user: input.user,
    database: input.database,
    password: input.password,
    port: input.port,
  });

  // A simple SELECT query
  try {


    await connection.execute("SET SESSION sql_mode = ''");

    const [results] = await connection.query(
      input.sql
    );

    connection.destroy();

    return {
      success: true,
      data: JSON.stringify(results),
      message: '成功'
    };
  } catch (err) {
    return {
      success: false,
      data: "[]",
      message: err.message,
    };
  }
};
当sql执行失败,把错误信息和sql传给大模型修复一下,然后再次执行。

在测试的时候,大模型生成的sql,有时候会出错,这里为了保险起见,支持再次生成sql,重新执行。

image.png

把sql查询出来的数据和用户的问题传给大模型,让大模型生成答案

image.png

大模型提示词

image.png

让大模型生成图表渲染参数和对数据进行分析

image.png

image.png

调用接口生成图表和大图

image.png

coze官方出了一个渲染图表插件,我没使用这个,是因为它生成的图表很模糊,所以我自己写一个生成svg格式的图表接口,原理是echarts服务端渲染。

生成大图使用的是svgdom库,使用node渲染svg,把图片和文本还是解析的内容合并到一个svg中,然后返回链接,用户点击链接可以在浏览器中查看。

服务端接口代码

image.png

image.png

格式化输出内容

使用代码节点格式化输出内容,因为coze的输出支持markdown格式,所以我这里就给格式化成markdown格式。

image.png

生成推荐问题

回答完用户当前问题,使用大模型根据当前问题和表格再生成三个推荐问题

image.png

绑定卡片

和前面两个工作流一样,给结束节点绑定推荐问题卡片。

效果预览

image.png

image.png

image.png

大图

image.png

这个工作流里还有一些校验和错误信息处理,因为不太重要,又比较多,我给省略了,所以大家看的可能觉得有点不连贯,大家见谅。

总结

到此整个应用使用到的所有技术都讲完了,如果对你有帮助,帮忙点个赞鼓励一下吧😊。

关于coze,说一下我的感想,我觉得我现在前端技术已经陷入了瓶颈,并且整个大环境对前端也不太友好,所以我想换个赛道试试。

开发coze应用算是我的一个尝试,个人感觉coze最后不一定能大火,但是这种模式未来肯定会成为热门,让普通人也可以做出AI应用,这次参赛获奖作品,有不少人都不是程序员,但是借助coze平台也做出好用的AI应用。

最后举个例子:

昨天和老婆孩子还有老婆的妹妹一起去吃饭,吃过饭服务员说在大众点评上写点评,会送甜品。

我老婆的妹妹和我们说,你们不知道怎么写点评,可以使用文心一言帮忙写。

我当时很诧异,她竟然也知道文心一言,她的职业是幼师,我一直以为这些大模型只有一些技术人员知道,没想到普通人都已经开始使用了,有点孤陋寡闻了。

通过这个例子我想说明,AI已经进入普通人的视野了,AI应用未来可能使用的人会越来越多,因为它真的能帮助人们解决一些问题,而AI应用开发会不会是下一个风口,这个我不知道,但还是先做一些准备吧。