使用coze重新做了一个升级版云顶之弈查询助手,功能更强大,交互性更强。

2,351 阅读9分钟

背景

前几天写了一篇文章,讲了我是如何使用coze实现云顶之弈助手的。不过后面留了一个需求,现在我们来实现一下,让插件功能更强大一点,交互性更强一点。

然而在上一篇说过表格模式下的知识库,回答用户问题不太稳定,所以这几天我一直在想怎么能让他稳定一点,后面我想到了一个方案,稳定率大大提升,下面和大家分享一下。

我的方案

上面说了coze自带的表格知识库回答的不稳定,是因为他生成的sql不太稳定,并且在工作流中使用知识库的时候,更不稳定。

我想了一下,既然coze默认生成的sql不稳定,那么我用大模型自己生成sql,然后动态执行sql查询结果。

大致流程如下

  1. 写一个服务从tactics网站抓取数据上传到自己数据库
  2. 写一个coze插件分析数据库表结构
  3. 在coze工作流中把数据库表结构信息和用户输入传给大模型,让大模型生成sql
  4. 写一个能够支持动态执行sql获取结果的coze插件
  5. 在coze工作流中把生成的sql传给sql执行器执行sql,获取结果

功能展示

查询棋子信息

image.png

image.png

image.png

查询装备信息

image.png

查询强化信息

image.png

查询棋子和装备

image.png

image.png

查询棋子和强化

image.png

image.png

问题

同一个问题,答案还是不太稳定,大家可以多提问几次。

image.png

提供的信息完善一点,准确率会更高一点

image.png

抓取数据上传到数据库

整理获取数据的接口

获取基本信息的接口

棋子信息: game.gtimg.cn/images/lol/…

羁绊信息: game.gtimg.cn/images/lol/…

职业信息: game.gtimg.cn/images/lol/…

装备信息: game.gtimg.cn/images/lol/…

强化信息: game.gtimg.cn/images/lol/…

奇遇信息: game.gtimg.cn/images/lol/…

获取实战数据的接口

d2.tft.tools/stats2/gene…

返回的数据中units字段里存放的是所有棋子的实战数据,包括(平均排名,前4率,登场次数,吃鸡率)

返回的数据中items字段里存放的是所有装备的实战数据,包括(平均排名,前4率,登场次数,吃鸡率)

获取强化实战信息稍微有点麻烦

需要先从https://tactics.tools/augments地址中获取html,然后使用cheerio库解析html里的数据。

// 获取强化实战数据
export const getHexData = async () => {
  const body = await fetch<string>('https://tactics.tools/augments');
  const $ = load(body);
  const data = $('#__NEXT_DATA__').text();
  const formatData = JSON.parse(data) as any;
  return (formatData.props.pageProps.augsData.singles || []).reduce(
    (prev, cur) => {
      prev[cur.id] = cur.base;
      return prev;
    },
    {}
  );
};

image.png

初始化本地项目

项目框架使用的是midway,数据库使用的是mysql,orm使用的是typeorm,使用corn起本地定时任务。

模型

棋子模型

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ comment: '棋子信息' })
export class Chess {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ comment: '棋子图片' })
  icon: string;

  @Column({ comment: '棋子名称或棋子名字' })
  chessName: string;

  @Column({ comment: '棋子种族' })
  race: string;

  @Column({ comment: '棋子职业' })
  occupation: string;

  @Column({ comment: '棋子技能名称' })
  skillName: string;

  @Column({ comment: '棋子技能描述', type: 'text' })
  skillDesc: string;

  @Column({ comment: '棋子的价格' })
  price: number;

  @Column({ comment: '平均排名', type: 'float', nullable: true })
  place: number;

  @Column({ comment: '前4率', type: 'float', nullable: true })
  top4: number;

  @Column({ comment: '第一名率(吃鸡率)', type: 'float', nullable: true })
  won: number;

  @Column({ comment: '上场、登场、上场次数或登场次数', nullable: true })
  count: number;
}

装备模型

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ comment: '装备信息' })
export class Item {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ comment: '装备图片' })
  icon: string;

  @Column({ comment: '装备名称' })
  name: string;

  @Column({ comment: '装备描述', type: 'text' })
  introduce: string;

  @Column({ comment: '装备英语名称' })
  enName: string;

  @Column({ comment: '平均排名', type: 'float', nullable: true })
  place: number;

  @Column({ comment: '前4率', type: 'float', nullable: true })
  top4: number;

  @Column({ comment: '第一名率(吃鸡率)', type: 'float', nullable: true })
  won: number;

  @Column({ comment: '上场、登场、上场次数或登场次数', nullable: true })
  count: number;
}

强化信息模型

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity({ comment: '强化信息' })
export class Hex {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ comment: '强化图片' })
  icon: string;

  @Column({ comment: '强化名称' })
  hexName: string;

  @Column({ comment: '强化描述', type: 'text' })
  introduce: string;

  @Column({ comment: '强化英语名称' })
  enName: string;

  @Column({ comment: '平均排名', type: 'float', nullable: true })
  place: number;

  @Column({ comment: '前4率', type: 'float', nullable: true })
  top4: number;

  @Column({ comment: '第一名率(吃鸡率)', type: 'float', nullable: true })
  won: number;

  @Column({ comment: '上场、登场、上场次数或登场次数', nullable: true })
  count: number;
}

棋子与装备的实战数据模型

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity({
  comment: '棋子装备数据表',
})
export class ChessItem {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ comment: '棋子名称' })
  chessName: string;

  @Column({ comment: '装备名称' })
  itemName: string;

  @Column({ comment: '平均排名', type: 'float', nullable: true })
  place: number;

  @Column({ comment: '前4率', type: 'float', nullable: true })
  top4: number;

  @Column({ comment: '第一名率(吃鸡率)', type: 'float', nullable: true })
  won: number;

  @Column({ comment: '登场次数', nullable: true })
  count: number;
}

棋子与强化的实战数据模型

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity({
  comment: '棋子强化数据表',
})
export class ChessHex {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ comment: '棋子名称' })
  chessName: string;

  @Column({ comment: '强化名称' })
  hexName: string;

  @Column({ comment: '平均排名', type: 'float', nullable: true })
  place: number;

  @Column({ comment: '前4率', type: 'float', nullable: true })
  top4: number;

  @Column({ comment: '第一名率(吃鸡率)', type: 'float', nullable: true })
  won: number;

  @Column({ comment: '上场 登场 上场次数 登场次数', nullable: true })
  count: number;
}

定时任务

使用corn起一个定时任务,每天晚上12点定时执行,把数据更新到数据库中,保证数据实时性。

image.png

image.png

编写获取数据库表结构信息插件

插件源码

使用mysql2连接数据库,然后执行sql查询表信息。

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,
  };
}

插件输入参数

image.png

输出参数

{
  "data": {
    "tables": [
      {
        "fields": [
          {
            "comment": "",
            "name": "id"
          },
          {
            "comment": "棋子种族",
            "name": "race"
          },
          {
            "comment": "棋子职业",
            "name": "occupation"
          },
          {
            "comment": "棋子技能名称",
            "name": "skillName"
          },
          {
            "comment": "棋子图片",
            "name": "icon"
          },
          {
            "comment": "棋子的价格",
            "name": "price"
          },
          {
            "comment": "棋子技能描述",
            "name": "skillDesc"
          },
          {
            "comment": "平均排名",
            "name": "place"
          },
          {
            "comment": "前4率",
            "name": "top4"
          },
          {
            "comment": "第一名率(吃鸡率)",
            "name": "won"
          },
          {
            "comment": "上场、登场、上场次数或登场次数",
            "name": "count"
          },
          {
            "comment": "棋子名称或棋子名字",
            "name": "chessName"
          }
        ],
        "tableComment": "棋子信息",
        "tableName": "chess"
      },
      {
        "fields": [
          {
            "comment": "",
            "name": "id"
          },
          {
            "comment": "棋子名称,不是外键不用联合查询",
            "name": "chessName"
          },
          {
            "comment": "强化名称,不是外键不用联合查询",
            "name": "hexName"
          },
          {
            "comment": "平均排名",
            "name": "place"
          },
          {
            "comment": "前4率",
            "name": "top4"
          },
          {
            "comment": "第一名率(吃鸡率)",
            "name": "won"
          },
          {
            "comment": "上场 登场 上场次数 登场次数",
            "name": "count"
          }
        ],
        "tableComment": "棋子强化数据表",
        "tableName": "chess_hex"
      },
      {
        "fields": [
          {
            "comment": "",
            "name": "id"
          },
          {
            "comment": "棋子名称,不是外键不用联合查询",
            "name": "chessName"
          },
          {
            "comment": "装备名称,不是外键不用联合查询",
            "name": "itemName"
          },
          {
            "comment": "平均排名",
            "name": "place"
          },
          {
            "comment": "前4率",
            "name": "top4"
          },
          {
            "comment": "第一名率(吃鸡率)",
            "name": "won"
          },
          {
            "comment": "登场次数",
            "name": "count"
          }
        ],
        "tableComment": "棋子装备数据表",
        "tableName": "chess_item"
      },
      {
        "fields": [
          {
            "comment": "",
            "name": "id"
          },
          {
            "comment": "强化图片",
            "name": "icon"
          },
          {
            "comment": "强化描述",
            "name": "introduce"
          },
          {
            "comment": "强化英语名称",
            "name": "enName"
          },
          {
            "comment": "平均排名",
            "name": "place"
          },
          {
            "comment": "前4率",
            "name": "top4"
          },
          {
            "comment": "第一名率(吃鸡率)",
            "name": "won"
          },
          {
            "comment": "上场、登场、上场次数或登场次数",
            "name": "count"
          },
          {
            "comment": "强化名称",
            "name": "hexName"
          }
        ],
        "tableComment": "强化信息",
        "tableName": "hex"
      },
      {
        "fields": [
          {
            "comment": "",
            "name": "id"
          },
          {
            "comment": "装备图片",
            "name": "icon"
          },
          {
            "comment": "装备名称",
            "name": "name"
          },
          {
            "comment": "装备描述",
            "name": "introduce"
          },
          {
            "comment": "装备英语名称",
            "name": "enName"
          },
          {
            "comment": "平均排名",
            "name": "place"
          },
          {
            "comment": "前4率",
            "name": "top4"
          },
          {
            "comment": "第一名率(吃鸡率)",
            "name": "won"
          },
          {
            "comment": "上场、登场、上场次数或登场次数",
            "name": "count"
          }
        ],
        "tableComment": "装备信息",
        "tableName": "item"
      },
      {
        "fields": [
          {
            "comment": "",
            "name": "id"
          },
          {
            "comment": "职业图片",
            "name": "icon"
          },
          {
            "comment": "职业名称",
            "name": "name"
          },
          {
            "comment": "职业描述",
            "name": "introduce"
          },
          {
            "comment": "职业英语名称",
            "name": "enName"
          }
        ],
        "tableComment": "棋子职业信息",
        "tableName": "job"
      },
      {
        "fields": [
          {
            "comment": "",
            "name": "id"
          },
          {
            "comment": "羁绊图片",
            "name": "icon"
          },
          {
            "comment": "羁绊名称",
            "name": "name"
          },
          {
            "comment": "羁绊英语名称",
            "name": "enName"
          },
          {
            "comment": "羁绊描述",
            "name": "introduce"
          }
        ],
        "tableComment": "羁绊信息",
        "tableName": "race"
      }
    ]
  }
}

插件已经上架到插件商店,可以使用数据库表结构分析搜索使用该插件。

编写执行sql插件

插件源码

使用mysql2库动态执行sql

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 }: 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,
  });

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

    connection.destroy();

    return {
      success: true,
      data: results as any,
      message: '成功'
    };
  } catch (err) {
    return {
      success: false,
      data: [],
      message: err.message,
    };
  }
};

输入参数

image.png

输出参数

image.png

插件已经上传到插件商店,可以使用sql执行器搜索使用。

编写云顶数据搜索coze工作流

image.png

获取完表结构信息后,通过代码把用户输入和表结构信息处理一下,方便大模型识别。

image.png

image.png

image.png

这里加了一个分支,如果生成的sql,执行失败,把报错信息和sql传给大模型,让大模型根据报错信息自动修改或优化sql再执行。如果成功直接把结果输出出去。

image.png

image.png

image.png

bot配置

image.png

所有的逻辑都在工作流中,所以bot配置比较简单。

安全性

因为涉及到执行sql,如果用户输入删除棋子表或删除棋子表数据,可能会把数据库中的表删除掉,我从两方面解决这个问题。

  1. 在生成sql的时候加限制
  2. 新建一个数据库用户,只给他分配select权限,delete,drop权限全部给限制了。

最后

大家如果对这个感兴趣,可以访问下面地址体验。

大家如果有建议,可以在评论区给我提需求,我后面会慢慢完善。

友情提醒:答案不一定完全正确,仅供参考。

www.coze.cn/store/bot/7…

Bot Id: 7370598857350348836