掘金 阅读 ( ) • 2024-04-25 09:07

使用coze扣子搭建智能bot「程序员的工具箱」的思考和总结

前记

大模型已经火了快 2 年的时间了,从简单的文字处理的单一场景到到现在的企业迫切需要 LLM 在更多的场景赋能的时代。大众也从简单问答、RAG 知识库、到智能 Agent 的构建有了自己的认识和想法。

如果要构建一个 Agent 就会涉及到一些架构模式:

路由分发架构模式

当用户输入一个 Prompt 查询时,该查询会被发送到路由转发模块,而路由转发模块则扮演着对输入 Prompt 进行分类的角色。

图片

大模型代理架构模式

在任何一个生态系统中,都会有多个针对特定任务领域的专家,并行工作以处理特定类型的查询,然后将这些响应整合在一起,形成一个全面的答案。

图片

基于缓存的微调架构模式

我们将缓存和微调引入到大模型应用架构中,可以解决成本高、推理速度慢以及幻觉等组合问题。

图片

面向目标的 Agent 架构模式

对于用户的 Prompt 提示词,Agent 会基于大模型先做规划(Planning),拆解成若干子任务,然后对每个子任务分别执行(Action),同时对每一步的执行结果进行观测(Observation),如果观测结果合格,就直接返回给用户最终答案,如果观测结果不合格或者执行出错,会重新进行规划(Replanning)。

图片

Agent 智能体组合架构模式

图片

从上述几种架构中我们不难看出还是非常复杂的,也不是一般企业能够做的了的。(费钱😭)

好在扣子已经把很多工作都做了。我们只需要在他上面去搭建想要的 agent 即可,大大减少了普通人的使用成本。

下面就结合自己的搭建一个 agent 的过程详细了解各个功能的构建过程。

工具集成

在扣子 bot 的编辑页面的中间区块插件区域我们可以选用扣子商店里面上架的工具。同时也可以自己去编写插件。插件的实现目前提供了两种语言:JavaScriptpython。可以根据自己的能力选用对应的语言编写即可。具体的编写插件的教程可以去官网文档查看。

那一个插件具体要实现什么样子的功能需要看我们构建的 bot 需要提供什么的能力。比如我是工具类的 bot,就需要集成各种常用的功能的工具。

目前我的bot 程序员的工具箱 里面集成了挺多的工具:红框住的都是自研插件,具体如何做一个插件可以参考官网文档:

image-20240424213506484

先分享一个自己实现工具 json2sql 将json数据转换为sql:(此插件目前已经上架到官方插件商店)

 import { Args } from '@/runtime';
 import { Input, Output } from "@/typings/json2sql/json2sql";
 ​
 /**
   * 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> {
   var json_str = input.jsonStr.replaceAll('\n', '')
   json_str = json_str.replaceAll("'",'"')
   var data = JSON.parse(json_str);
   var sql = "";
   sql += "CREATE TABLE {table_name} (\n";
   var fflag = 1;
   var field = [];
   for (const key in data) {
     if (key == "id") {
         fflag = 0;
         continue
     }
     let val = data[key];
     if (typeof val == "string") {
         field = field.concat("  `" + key + "` VARCHAR(255) NOT NULL default '' ");
     }
     if (typeof val == "number") {
       if ((val + "").indexOf(".") > -1) {
         field = field.concat("  `" + key + "` FLOAT ");
       } else {
         field = field.concat("  `" + key + "` INT NOT NULL default 0 ");
       }
     }
     if (typeof val == "boolean") {
         field = field.concat("  `" + key + "` BOOLEAN NOT NULL default false ");
     }
   }
 ​
   if (!fflag) {
     field = field.concat("  `id` int(11) NOT NULL AUTO_INCREMENT");
     field = field.concat("   PRIMARY KEY (id)")
   }
   logger.info(field);
   sql += field.join(",\n");
   sql += " \n );";
   return {
     result: sql,
   };
 };

输出:

 CREATE TABLE {table_name} (
   `name` VARCHAR(255) NOT NULL default '' ,
   `age` INT NOT NULL default 0 ,
   `sex` VARCHAR(255) NOT NULL default '' ,
   `phone` VARCHAR(255) NOT NULL default '' ,
   `address` VARCHAR(255) NOT NULL default '' ,
   `money` FLOAT ,
   `deleted` BOOLEAN NOT NULL default false ,
   `id` int(11) NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (id) 
  );

让模型调用工具

不管工具的能力多好,能让大模型调用到才是真的好🤣。

如何才能让大模型调用到呢?那就是 prompt 的编写。

扣子官方提供的案例里面是使用 markdown 格式写的,那我们也参考官方的格式:

 ### 技能 3: json 数据转 sql 
 1. 如果用户需要将 json 数据转换为 sql 语句,运用 json2sql 工具进行转换。
 2. 不要对输出的 sql 做改写,输出 markdown 格式。

这里主要是对模型说明当遇到什么样子的数据时候需要调用哪个工具来处理数据。所以我们调用一个工具需要注意两点:

  • prompt 中需要以简单直接的语句说明工具调用的方式
  • 在工具的说明里面也需要说明工具的能力

这些内容都会提交给大模型去理解,观察、执行。也就是 React 的执行能力。如果想要构建一个比较智能的 Agent 就需要 React 的思考执行模型。感兴趣的可以去了解下大模型应用中的 React

查询Linux命令的能力:

我们还是先看使用效果:

示例 1: 查询 sed 命令

image-20240424085642354

示例2: 查询 grep 命令

image-20240424204538055

解析:

linux命令的查询以及使用能力大模型已经具备了一些能力,不过我这里还是使用了知识库的能力作为 LLM 的内容补充。总共有 600+的命令作为知识库的内容。

image-20240424205112727

LLM 会存在一些痛点,比如知识老旧、幻觉、高成本。所以很多情况下需要使用 RAG 的技术来解决 LLM 的问题。当然这里也不例外,也是利用了 RAG 的技术。

RAG 有三方面的好处:

  • 确保 LLM 可以回答最新、最准确的内容。并且用户可以访问模型内容的来源,确保可以检查其声明的准确性并最终可信。
  • 通过将 LLM建立在一组外部的、可验证的事实数据之上,该模型将信息提取到其参数中的机会更少。这减少了 LLM 泄露敏感数据或“幻觉”不正确或误导性信息的机会。
  • RAG 还减少了用户根据新数据不断训练模型并随着数据的变化更新训练参数的需要。通过这种方式企业可以减低相关财务成本。

关于 LLM 与 RAG 的详细内容这里不再赘述,大家可以参考我前面的文章RAG实操教程: langchain+Milvus向量数据库创建你的本地知识库

自己干

我这里的 600+的 linux 命令对应的都是采集的网页数据。扣子提供了网页自动采集的能力。但是他只能一条条采集。对于600+的网页数据在没有 API 的基础上去搞估计的废了😭。文件上传可以一次导入 10 个文本,这个效率感觉比上面的那个靠谱多了。

于是就动手自己去采集 600+的网页数据。采集数据需要注意几点:

  • 限制采集的频率
  • 添加必要的 header 参数
  • 如果有用户认证的需要对应的 cookie 或者 token。
  • 提取网页上面自己关心的数据内容。

所以奔着一切动手实践的初衷手写爬取网页的代码,并按照每 10 个文件保存到一个目录中。

下面是 python 代码,需要的同学可以参考:

 from langchain_community.document_loaders import WebBaseLoader
 from bs4 import SoupStrainer
 from bs4 import BeautifulSoup
 import os, time
 ​
 def linux_shell():
     total = 1
     for idx in urls:
         url = idx
         url_arr = url.split('/')
         name = url_arr[-1].replace('html', 'txt')
         # 定位网页上面需要爬取的数据块
         only_a_tags = SoupStrainer(class_="markdown-style")
         loader = WebBaseLoader(
             web_path=url,
             encoding='utf-8',
             bs_kwargs={'parse_only': only_a_tags}
         )
         # 这里为了简单就直接使用langchain 的文本加载器
         data = loader.load()
         dir_name = f'/Users/oo7/Developer/langchain/coze/cmds/{int(total/10)+1}'
         if os.path.exists(dir_name) is False:
             os.makedirs(dir_name)
         file_name = f"{os.path.join(dir_name, name)}"
         # 简单处理文件的格式
         page_content = data[0].page_content.replace('\n\n\n\n', '\n')
         # 保存内容到文件中
         with open(file_name, 'w') as f:
             f.writelines(f"Linux {name.split('.')[0]} 命令详解 \n")
             f.write(page_content)
         
         print(f"{total}: {idx} is downloaded")
         time.sleep(0.5)
         total += 1

获取新闻能力

这里主要是使用了获取新闻的插件:ithome_news 这个插件是自己做的(现已上架到扣子官方的插件商店中,有需要的可以自行下载),数据来源是爬取某科技网站的日榜周榜月榜的新闻。

工作流构建:

构建工作流需要的节点:

  • 开始节点
  • LLM 节点
  • 选择器节点 (好像没必要了😄)
  • 插件节点(调用获取新闻)
  • 结束节点

image-20240424090524773

这里流程图里面两个节点很关键:

  • 大模型节点:这个节点让大模型去理解用户输入内容,并分析需要获取新闻的分类是哪一个。

     ## 返回
     新闻类型:日榜、周榜、月榜。
     从用户输入内容"{{input}}。默认类型为日榜" 中提取新闻类型.返回的内容只能是新闻类型中的内容。
     - 类型参数:{{category}}
     ​
    

    image-20240424090946299

    这一步很关键,目的就是让大模型能准确获取用户内容的意图从而提取里面的分类。这个分类会传递到后面的新闻节点,然后输出对应的类型。

  • 新闻工具节点:节点从LLM获取到的输入分类参数,输出对应分类下面的新闻。如果没有大模型提取出的新闻分类,这一步就很鸡肋。

    image-20240424091335437

    从上面的测试结果看,已经很完美的输出了数据。

  • 看下使用效果

    image-20240424091739486

使用工作流

image-20240424091946013

bot 中使用工作流 news_flow

image-20240424092128400

想让 bot 展现的时候是卡片的效果,只需要将工作流输出的内容数据以卡片的形式展现即可。

image-20240424092613954

对于卡片输出的注意点:

  • 输出的内容必须是固定的,绑定卡片的数据才能固定显示。一般使用API等程序输出固定格式内容
  • 多字段的数据,比如具有标题字段、内容字段、图片字段、url字段的数据会让效果更好。

定时任务能力

这次意外发现,官方提供了一个触发器的能力。那就是设置定时任务,到时间自动执行。有了这个功能是不是突然感觉发挥的空间就很大了呢?

image-20240424224158307

prompt 的重要性

prompt 的重要性相信好多人都认识到了。扣子的 prompt 使用 markdown 格式编写。主要分为三个部分。

  • LLM 的角色设定以及所具备的能力。
  • 详细说明每一个能力,以及能力触发的场景和对数据的处理能力。
  • 约束:限制 bot 不能做什么。

如果效果不是很理想需要不断的调整 prompt,目的是让 LLM 里面我们的意图。

除此之外工具、工作流等的名称以及描述也是整个 botprompt 的一部分。到最后 bot 去执行的时候都会将相关工具的描述拼接到 prompt 里面。最后让 LLM 去思考、执行、观察一直不断的循环这个过程,直到 LLM 认为需要停止的时候输出最终内容。下面的图是 React 的整个过程。

image-20240424232450134

其他功能:

主要的功能讲完后剩余的就是其他功能了,下面的这些都是辅助功能,让你的 bot 更加的完美。

  • 开场白文案:也就是自我介绍👀。
  • 开场白预置问题:引导用户提问,使用户能够快速掌握相关的能力。
  • 用户问题建议:
  • 语音:个性化,依据场景以及个人爱好设定就行。

具体如何使用请查询官方文档。

总结:

上面带大家熟悉了构建一个 bot 的整体过程包括:

  • LLM agent 架构模式。
  • 插件的编写功能实现,以及插件 prompt 的重要性。
  • 知识库构建,知识库与 bot 的结合。RAG 相关知识。
  • 流程 flow 的使用。

相关文章:

欢迎大家使用 bot: 程序员的工具箱, 如有想法可以评论留言讨论!

Bot id: 7360782245814190080