大模型,小探索 —— AI 帮写个飞书插件,赚个"零花钱" ^_^

微信公众号内容地址: https://mp.weixin.qq.com/s/NHRTEBYb29hLnoaiNZPFpg

AI 协同的三种模式: Embedding 嵌入Copilot 副手Agent 智能体

随着探索的逐层深入,目前基本实验完成了大模型的 API 调用、部署和微调。

本着“既要钻研,也要吃饭,还要可持续”的思路,小狗屁这次打算探索一下 AI 协同 + 飞书插件变现

AI 协同

未来的竞争,是 使用 AI 进行提效对 AI 结果进行价值判断 的能力的竞争。

举例来解释上图,假设小刀同学擅长服务端编程和项目整体结构,那么他与 AI 的协同方式可能会是:

  • Embedding——帮忙处理擅长的事情:GPT 根据细节要求帮忙写一部分 Python 或 Java 代码,帮助小刀节省时间
  • Copilot——帮忙处理不擅长的事情:GPT 根据项目需求帮忙写完整的前端网页代码,小刀再拿来和服务端整合完成项目,提升人效
  • Agent——帮忙处理有挑战的事情:GPT 根据业务理解帮忙设计、训练和部署推荐算法,供项目接入,增强能力

需要指出的是,即使是相同水平的 AI,对不同的人来说可能也会属于不同的协同方式

变 现

飞书多维表格 & 插件

https://www.feishu.cn/product/base

相比于 Excel,多维表格可以提供更强的在线协同能力;相比于数据库,多维表格可以提供更强的可视化展示交互能力。

整体效果与在线表格类似,最右侧提供了插件功能。

插件开发和激励计划

插件给多维表格带来了更多操作和可能性,而激励计划也让小狗屁看到了通过大模型的协作来变现的可能 😏。

多维表格插件开发

需求

结合表格记账、运行插件计算结果的场景,小狗屁很快想到一个 idea

  • 分账计算:即基于付款人和付款金额的表格,计算给出 AA 分账、互相转账的策略。

初步设计

总体设计思路也比较简单,

  1. 开发环境和部署测试流程:开发指南中有写,适合人工完成。
  2. 表格数据读取为变量:同样在开发指南中有对应的操作,适合人工完成。
  3. 基于输入,使用核心分账算法得到输出:为功能核心代码,且不算很难、易于测试,与外部环境独立,适合 GPT 完成;且可以与上一步骤对调,上步骤中的输入处理,向 GPT 给出的代码对齐
  4. 将输出展示在表格中:需要在开发指南中找到对应的操作,适合人工完成。
  5. 提交插件、与飞书工作人员沟通:暂时只能人工完成。

开发环境

参照开发指南,使用 Replit 在线开发,同时飞书也提供了 UIBuilder 模板,只需要在 runUIBuilder.tsx 文件中写输入、计算、输出逻辑即可。

部署测试

参照开发指南,Replit 运行后会得到临时链接,同时在飞书多维表格中用该临时链接创建临时插件,进行测试。

需要留意以下两点:

  • 需要支持国际化能力:参照 UIBuilder 的开发手册,即适配中文和英文。
  • 编译:Typescript 相比于 Javascript,类型编译要求较高,因此运行之前,在 Replit 上使用 tsc && vite build 进行校验。

分账功能

具体代码细节如下:

function splitExpenses(transactions) {
  // transactions是一个包含每次支付信息的数组,每个元素是一个对象,包含payer(支付者)、amount(支付金额)、participants(参与者数组)

  // 创建一个对象来跟踪每个人的余额
  let balances = {};

  // 计算每个人的余额
  transactions.forEach(transaction => {
    // 支付者扣除支付金额
    balances[transaction.payer] = (balances[transaction.payer] || 0) - transaction.amount;

    // 参与者每人增加平均支付金额
    let perParticipantAmount = transaction.amount / transaction.participants.length;
    transaction.participants.forEach(participant => {
      balances[participant] = (balances[participant] || 0) + perParticipantAmount;
    });
  });

  // 找到余额最大的人和最小的人
  let maxBalancePerson = Object.keys(balances).reduce((a, b) => balances[a] > balances[b] ? a : b);
  let minBalancePerson = Object.keys(balances).reduce((a, b) => balances[a] < balances[b] ? a : b);

  // 将最大余额的人付款给最小余额的人,直到两者之一的余额接近零
  while (balances[maxBalancePerson] > 0.01 || balances[minBalancePerson] < -0.01) {
    let amountToTransfer = Math.min(Math.abs(balances[maxBalancePerson]), Math.abs(balances[minBalancePerson]));

    // 更新余额
    balances[maxBalancePerson] -= amountToTransfer;
    balances[minBalancePerson] += amountToTransfer;

    // 打印每次的转账情况
    console.log(`${maxBalancePerson} 支付 ${amountToTransfer.toFixed(2)} 给 ${minBalancePerson}`);

    // 重新计算最大和最小余额的人
    maxBalancePerson = Object.keys(balances).reduce((a, b) => balances[a] > balances[b] ? a : b);
    minBalancePerson = Object.keys(balances).reduce((a, b) => balances[a] < balances[b] ? a : b);
  }
}

// 示例使用
const transactions = [
  { payer: "PersonA", amount: 30, participants: ["PersonA", "PersonB", "PersonC"] },
  { payer: "PersonB", amount: 20, participants: ["PersonA", "PersonB", "PersonC"] },
  { payer: "PersonC", amount: 10, participants: ["PersonA", "PersonB", "PersonC"] },
];

splitExpenses(transactions);

输入部分

由分账功能的代码可以看出,需要将表格数据转换成 transactions 字典数组。因此,输入部分主要分为两步:

  • 提供插件的界面,让用户指明付款人和付款金额字段;
  • 以及将这两列的数据挨个读取,并转换成目标数组
export default async function main(uiBuilder: UIBuilder, { t }: UseTranslationResponse<'translation', undefined>) {
  uiBuilder.form(form => ({
    formItems: [
      form.tableSelect('table', { label: `${t('Select data table')}` }),
      form.viewSelect('view', { label: `${t('Select view')}`, sourceTable: 'table' }),
      form.fieldSelect('payerField', { label: `${t('Select field for payer')}`, sourceTable: 'table', multiple: false }),
      form.fieldSelect('amountField', { label: `${t('Select field for amount')}`, sourceTable: 'table', multiple: false }),
      form.select('modeSelect', { label: `${t('Select the way to show results')}`, options: [{ label: `${t('Only Text')}`, value: 'Text' }, { label: `${t('Text + new field in table')}`, value: 'Table' }], defaultValue: 'Text' }),
    ],
    buttons: [`${t('Submit')}`],
  }), async ({ values }) => {
    const { table, view, payerField, amountField, modeSelect } = values as { table: ITable, view: IView, payerField: ITextField | IUserField, amountField: INumberField | ICurrencyField, modeSelect: string };
    /**
       基于借贷记账法,实现分账功能;保持每次支付都是由一个人单独支付,但是通过互相转账,实现每个人都支付了相同的金额
    */
    const recordIdList: (string | undefined)[] = await view.getVisibleRecordIdList();
    // 支付记录 payer:amount
    let transactionMap: { [key: string]: number } = {}
    // 遍历,并更新支付记录
    for (let i = 0; i < recordIdList.length; i++) {
      const payerValue = await payerField.getValue(recordIdList[i] as string);
      const amountValue = await amountField.getValue(recordIdList[i] as string);
      if (isNullOrUndefined(payerValue) || isNullOrUndefined(amountValue)) {
        continue;
      }
      // 付款人 支持 文本字段 || 人员字段
      const payer = (payerValue[0] as IOpenTextSegment).text! || (payerValue[0] as IOpenUser).name!
      // 金额 支持 货币字段 || 数字字段
      const amount = amountValue;
      transactionMap[payer] = (transactionMap[payer] || 0) + amount;
    }
    // 生成transactions transactions = [{ payer: "PersonA", amount: 30, participants: ["PersonA", "PersonB", "PersonC"] },...]
    const transactions = Object.keys(transactionMap).map(payer => {
      return {
        payer,
        amount: transactionMap[payer],
        participants: Object.keys(transactionMap)
      }
    })
    // 调用分账计算算法
    const results = splitExpenses(transactions);
    ...

输出部分

同样由分账算法可知,输出的内容其实也是一个三元字典数组,每一行是是 payer 需要付给 payee 的金额为 amount。

// text输出结果
results.forEach(result => { uiBuilder.text(`${result.payer} ${t('pay')} ${result.amount} ${t('for')} ${result.payee}`) });

整体效果

总 结

本次分账计算插件的开发过程中,AI 的协同模式可以认为是Embedding,主要帮助缩短了开发时间

实际上,利用微调,还可能更进一步,即让 GPT 完成开发指南中内容的学习,进而尝试完成整套代码的开发,而人则可以解放双手,负责创意和想法。这就有待小狗屁的继续探索了~.~。