我的技术和生活

  • 使用 CRXJS Vite 插件开发 ChatGPT SidePanel 插件(一)

    使用 CRXJS Vite 插件开发 ChatGPT SidePanel 插件(一)

    OpenAI DevDay 简单回顾

    OpenAI DevDay 上发布了一大堆新特性新功能,提升上下文容量、降低 token 价格,再次震撼整个行业。相信大家已经通过各种渠道了解到这次更新的细节,所以我就不再赘述。这里简单分享三个观点:

    1. 这次更新最值得关注是 Assistant API,因为这项功能大大降低了 AI 工具的开发门槛,让很多开发者不需要学习了解新技术,就能上手开发较复杂的 AI 应用。
    2. TTS、DALL-E 3 API 开放后,OpenAI 开发生态基本完整。ChatGPT 可以开口说话,也可以动手画画;再加上前面说的 Assistant API 所带来的 Retrieval 和 Code Interpreter,ChatGPT 可以拥有训练集以外的知识,也可以拥有 LLM 以外的逻辑思维能力。产品实现上的卡点基本打通,剩下就是应用层开发扩展了。
    3. $20/月的 ChatGPT Plus 价值进一步提升,俨然已经是性价比之选。如何好好利用,将其价值压榨出来,值得我们思考。我的想法是通过浏览器扩展加强自动化与可编程性。

    Chrome Extension SidePanel API (Chrome 114+)

    Chrome 浏览器从 v114 之后,开始支持 SidePanel,从此我们可以把扩展放在浏览器侧边栏里,提供新的可能性。

    之前我们使用扩展时,有三种方案,它们都有一些影响使用的问题:

    1. Popup:非常容易被关闭,基本上只要 popup 窗体失焦,就会被关闭,里面执行中的程序也会停下来。
    2. Content Script 插入 DOM:新插入的 DOM 可能跟原本的页面有冲突,尤其是样式,会增加开发成本。
    3. 独立打开:需要成为 activeTab,无法与目标页面共存。

    这些问题都可以被 SidePanel 很好地解决。于是,我们可以利用 SidePanel API 开发一个浏览器扩展,它可以大幅加强某个网站的功能、提升在这个网站里执行自动化操作的能力。我们不用担心它会被以外关闭,导致自动化失效;也不用担心它会和目标网页产生冲突。

    假如,我们针对 ChatGPT 网站开发一个扩展,加强它的功能,把 ChatGPT Plus 的功能和额度用好用满,应该可以实现一些相当不错的功能。

    CRXJS Vite 插件改进浏览器扩展开发

    产生上述想法之后,我就一直想找机会试试。不过开发浏览器扩展还有个痛点:扩展拥有加强版 API,在普通页面里无法使用;但是如果使用开发者模式加载扩展,又会丧失 HMR,开发不便。

    经过调研,发现 CRXJS Vite 插件 可以解决这个问题。它可以给插件开发环境添加 自动更新的功能,我们就不需要每次更改代码之后再手动刷新,也可以确保我们的开发环境支持全套 chrome.* API,与实际运行环境一致,大大提升我们的开发效率。

    使用该插件的方式非常简单。首先,创建一个 vite 项目。对我来说,效率最高的框架还是 Vue3。本着每次尝试的新技术不要超过 1/4 的比例,那就 vue-ts 吧:

    pnpm create vite my-vue-app --template vue-ts

    接下来,安装并配置 crxjs vite 插件:

    pnpm i @crxjs/vite-plugin@beta -D

    然后配置 vite.config.ts

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import { crx } from '@crxjs/vite-plugin'
    import manifest from './manifest.config';
    
    export default defineConfig({
      plugins: [
        vue(),
        crx({ manifest }),
      ],
      // 注意,这段配置很关键,请保证开发端口与 hmr 端口一致。不知道为何,插件生成的扩展里缺少 5173 默认值。
      server: {
        strictPort: true,
        port: 5173,
        hmr: {
          clientPort: 5173
        },
      },
    })

    我的 manifest.json 也是使用 TypeScript 生成的,所以上面我 import 本地的 manifest.config.ts 文件。

    export default defineManifest(async function (env) {
      return {
        "manifest_version": 3,
        "name": "my ChatGPT tools",
        permissions: [
          'activeTab', // 要往目标页注入脚本
          'scripting',  // 同上
          'sidePanel',  // 启用 sidePanel
          'tabs', // 为了与 content script 通信
        ],
        content_scripts: [
          {
            matches: ['https://chat.openai.com/*'],
            // crxjs 会帮我们把目标文件编译后注入目标页面
            js: ['./content/src/index.ts'],
          },
        ],
        // 针对 ChatGPT 而做
        host_permissions: [
          'https://chat.openai.com/*',
        ],
        // 启动 sidePanel 时,加载当前项目的页面
        side_panel: {
          default_path: 'index.html',
        },
        // 这里主要为了点击图标能打开或关闭 sidePanel,background script 同样交给 crxjs 处理
        background: {
          'service_worker': 'src/sw.ts',
          'type': 'module'
        },
      };
    });

    配置完成之后,照常启动项目 pnpm run dev

    然后在浏览器的扩展管理器里启动开发者模式,加载已解压的扩展目录即可。

    CRXJS 插件原理

    启动开发环境之后,CRXJS 会帮我们生成一个开发版的浏览器扩展,里面除了必备文件之外,还有各大组件所需的加载器,帮我们分别加载 service worker、content script,和页面内 js。它还会建立一个 WebSocket 连接到 vite 开发服务器,当侦听到目标文件出现变化时,就通过各种方式重新加载。比如,页面文件可以直接 HMR,service worker 可能要刷新组件,而 content script 甚至要刷新目标页。

    于是,便实现了浏览器扩展在开发环境下的 HMR。

    使用 CRXJS 开发浏览器扩展的注意事项

    首先,前面代码里有写,需要注意配置 HMR 端口。不知道为何,CRXJS 不使用默认的 5173 端口。

    其次,content script 需要在目标页面执行,所以 content script 修改后,常常需要刷新目标页。但是不知道什么原因,有时候 CRXJS 自动刷新目标页之后,content script 并没有更新,我猜测与这几步操作的执行顺序有关。我建议用开发者工具打开 content script 确认一眼。

    比如我的 content script 是 content/src/index.ts,那么就确认 content/src/index.ts.js 即可。

    以及,由于 HMR 可能会更新运行环境,如果此时恰逢我们在使用 chrome.tabs.sendMessage() 传递消息,可能导致 SidePanel 页和目标页连接断开,消息传送失败。解决方案嘛,就是多重启。修改完消息两端的代码之后,连目标页带侧边栏一起重启一次,即可。

    总结

    目前我的扩展还在开发中,将来做好了可能会上线 CWS,暂时就先不公开仓库了。

    新技术总能带来新的可能性,希望大家都能抓住这一波机会,无论是 OpenAI、LLM 还是浏览器 SidePanel 扩展,做出有价值的产品。

    有任何问题、建议、想法,欢迎留言讨论,共同进步。

  • 使用 Turnstile 保护网站

    使用 Turnstile 保护网站

    Turnstile 是 Cloudflare 近期推出的一项免费验证码服务,可以用来验证当前用户是否是真人。与 CAPTCHA 不同,它不要求我们使用 Cloudflare 网络,所以任何网站都可以使用这项服务。对我这种 Vercel 重度用户来说,Turnstile 非常合适。

    对初创 AI 产品来说,我们既怕用户滥用我们的服务,毕竟每个 token 都要花钱(甚至要花不少钱);又怕复杂的登录注册会把用户吓跑。于是验证码就成为了比较好的选择,毕竟,只要不是自动化批量请求,自然人用户随便用用,基本上不会产生问题。

    Turnstile 使用 JS 脚本收集用户在当前网页上的交互,并通过一些深度学习的模型来判断当前用户是否是真人,这样一来就可以减少验证码出现的频率,提升用户体验。另外,免费版 Turnstile 可以不限次数地使用,对于我这种白嫖党来说,也是重大利好。

    所以,综上,我近期给公司的 API 加上了 Turnstile,以避免可能产生的滥用。

    使用 Turnstile 非常简单,这里简单说一下。

    1. 首先,我们要注册一个 Cloudflare 用户。这一步应该有手就行,我就不多说了。
    2. 接下来,创建 Turnstile 应用。
    3. 然后复制保存 Site Key 和 Secret Key。
    4. 在项目中集成 Turnstile。

    Turnstile 的工作逻辑是这样的:

    1. 我们往页面里嵌入 Turnstile JS
    2. Turnstile JS 会收集用户的各种信息,作为判断用户是否是真人的依据
    3. 当用户需要与 API 交互的时候,Turnstile JS 会生成一个 token,用户需要把 token 一起提交给 API
    4. API 拿到 token,去 Turnstile Server 检查该 token 代表的用户是否是真人
    5. 如果是,就继续执行;如果不是,就返回错误
    6. 当然,还有一些衍生逻辑,比如验证来源,token 超时失效等等

    上面这些逻辑手动实现并不复杂,如果有现成的库,使用起来就更简单。我厂的 SoulScript 使用 Nuxt3 框架,有官方推荐的模组 @nuxtjs/turnstile,所以我只要按照文档进行配置,就可以在项目当中使用 Turnstile。

    不过实际测试之后,我发现还是有点问题:

    1. 免费版(Managed)的验证码窗口不是太好看,体积也不小,插入高设计感的网站,视觉效果不理想
    2. 如果在用到验证码的时候再检查用户状态,需要花费几秒到十几秒的时间,会耽误用户干正事。

    所以我加了个开关,没发现问题就先关上,等用量上去的时候再开。


    以前我比较倾向用户体系和注册登录,因为这样可以收集更多用户信息,也比较方便我们控制用户的行为。不过现在,流量越来越贵、用户越来越难搞,同时,免费服务、Serverless 越来越多,也许,local storage + 真人验证才是下一阶段我应该首选的策略。

  • 聊两句广州开源人聚会(2023-10-21)

    聊两句广州开源人聚会(2023-10-21)

    我不记得怎么关注的 @tison,印象里好像是有人在聊开源,我看到觉得不错就关注了。他上周六号召了一场广州开源人聚会,我一看 TiDB(PingCAP)赞助了场地,路线熟悉,这个周六也没什么安排,就报名参加了。

    这场活动由 开源社TiDB 赞助。我和两家都有些交集。TiDB 自不用说;去年 Google 主办的 Code for better _ Hackathon 后,我们几个获奖选手都应邀去 开源社开源年会 做了演讲。不过说来惭愧,我对开源的贡献不多,虽然我的代码大都放在 GitHub 上面,并且使用 MIT 协议,但实际上没有推广、没有测试、也没有文档,所以真心算不上什么贡献。

    所以我去参加活动的时候也很忐忑,很怕大家一盘道,原来我是作品做少、用户最少的一个。不过很明显是过虑了:这是场蛮典型的程序员聚会,大家大多默默走进房间,默默坐下,默默拿出手机开始刷。只有几个人在高谈阔论,如此社牛很明显是主办方,果然,其中一人便主持人 @tison。

    @tison 分享了他从事开源的经历。真好呀,年轻又厉害。他的早期经历跟春哥有点像,也是大学期间从 Perl 开始。毕业后经历过大厂,但更多的还是在开源商业公司做开发,目前在 StreamNative。他参与了很多项目,现在竟然还在给 Answer 作 mentor,虽然年纪很轻,但真的非常厉害。

    我对他的发言印象最深的是:

    1. 开源项目很多都是少数几个核心骨干做很多工作,其他贡献者可能付出寥寥
    2. 创始者的风格会对开源项目带来很大影响,比如 Perl,就很像一门宗教
    3. 他接手开源项目,或者做 mentor,起手式是文档、CI、测试。我觉得这点很好,值得我学习。

    接下来,另外两位开源商业项目的从业者上台,分享他们的开源经历。这两位就比较让我感到亲切了,除去为公司工作写开源项目外,他们分享的也是给文档挑 typo 这种经历,哈哈。

    接下来聊聊开源软件的商业化。我对这种商业模式很熟悉,毕竟当年我在 OpenResty Inc. 工作过,而我们正是开源商业公司。我们的工作模式是:

    1. 维护开源软件
    2. 售卖软件服务

    开源软件可以很容易地接触到尽可能多的用户,培养他们的使用习惯,在他们的使用过程中捕获错误、改进产品。找到机会成为事实上的行业标准,然后整个行业就离不开这个软件,最典型的例子就是 Linux。

    作为软件的核心开发者,创造软件的公司自然可以享有更高的话语权;也更能说服此软件的使用者:如果你们需要进一步的服务,找我们准没错。这就相当于把传统企业用在市场上的费用,拿来支持开源软件开发,所以商业上也说得通。

    以前我只知道国外有 WordPress、Ghost、RedHat,这次见到国内的开源商业公司也都在茁壮成长,感觉很高兴。

    不过呢,这些公司(包括 TiDB)的目标领域有些过分单一:基础设施。数据库、不同种类的数据库、网关(API6),等,都是基础设施。原因我猜很简单,因为基础设施最可能拿到稳定的收入。这些软件,即使使用开源版本,都有很高的配置和使用门槛,更不要说后期维护、升级。如果是普通 2c 软件、SaaS 软件开源,可能更多的是拿去套壳做二次贩售吧……

    总之吧,我觉得,前途仍然是光明的、充满期望的,希望中国开源软件越做越好。

    感谢各位赞助商,开源社、TiDB,主办方 @tison,希望下次我能找到足够多的东西分享给大家。

  • 尝试用 DALL-E 3 制作漫画《姆伊姆伊》

    尝试用 DALL-E 3 制作漫画《姆伊姆伊》

    DALL-E 3 发布有一阵子了,今天跟一个朋友聊到 AI 制作漫画,我突然想起来,还没测试过 DALL-E 3 的漫画制作能力,所以简单试了试。我认为它的表现不错,做绘本类的产品应该问题不大。下面是我的作品。

    Once upon a time, there is a dog, his name is Muimui

    (这是我早年帮孩子写的英文演讲的作文。)

    Once upon a time, there is a dog, his name is Muimui. Muimui is a corgi.

    Muimui is a good dog, everything is fine except that he like barking very much.

    When I cleaning the room, he barks.

    When my mom and dad talk to me loudly, he barks.

    One day, I was eating hot dog bread, I like hot dog bread.

    Suddenly, my mom said to me: “Do you finish your homework?”, when I turn my head to her, I fall the hot dog bread down to the ground!

    (这张图我尝试了很多次,都没法把说话气泡指向妈妈。)

    I was shocked, and my hot dog bread fell to the floor.

    Muimui run fast to the hot dog bread immediately, grabbed the bread and happily ate it.

    This time, he didn’t bark at all!

    简评 DALL-E

    DALL-E 作为 OpenAI 的产品,它最大的优势就是可以很好的理解我们的 Prompt,不需要我们事先学习模型训练时的标记,大大降低了普通用户使用 text2img 功能的难度。Prompt 会试图把我们的 Prompt 扩写得更全面,比如第一幅图,实际生效的 Prompt 是:

    Watercolor painting of Muimui the corgi looking out of a large window in a comfortable home, with curtains gently swaying.

    如果我们要调整图片,可以继续跟 DALL-E 对话,它会自动帮我们整合 Prompt,然后输出新的图片。我们可以要求它基于某个图片进行修改,以便产出我们想要的效果。

    如果你仔细观察,会发现上面几张图片里,有几张里面的角色存在明显的特征,比如坐在沙发上的那个小男孩,戴眼镜穿衬衣,卡其色的裤子都很接近,我不知道它是怎么做到的,不过如果能维持这个稳定性,可能真的可以用来画漫画。

    我并没有尝试对角色稳定性做出约束,比如什么样的发型、什么样的衣服等,不知道会不会对输出的结果产生影响。

    不过 DALL-E 也有劣势:我们不能自己训练小模型(Lora),所以大家的产出可能会千篇一律。另外 DALL-E 有很多“安全性”预设 Prompt,我们不能随意要求它画出一些 NSFW 的作品。

    总结

    不知道读者觉得这个作品如何?欢迎留言讨论。

  • AI 产品开发小科普

    AI 产品开发小科普

    今天有个朋友问我,有没有本地运行过 AI 模型,他想做一个 text2sql 工具,自动分析数据库,然后就能自然语言查询,并进一步做智能分析,乃至逻辑推演。我分享了一些知识、理解给他。我发现,虽然我一直觉得自己懂得少,没啥可分享的;但是总有朋友比我懂得更少,需要学习和纠正。所以写一篇小科普,希望帮到大家。

    text2sql 产品的问题

    类似的产品比较多见,比如早期的明星作品 ChatExcel.com,现在 TiDB、Supabase 也都提供了 AI 生成 sql 的功能。不过我觉得都不好用,原因如下:

    1. 控制不住幻想。
    2. 没法很好的理解数据库结构,无法根据我的那些可能并不准确的字段名推断表之间的关系,并合理的输出 SQL。
    3. 没有根据环境微调,输出的代码可能没法直接跑。比如 TiDB 也会输出 PG SQL。反之亦然。

    幻想”是目前 LLM 最常见的问题。我们可以把 LLMs 理解成小学生,他们迫切地想满足用户(老师、家长)的要求(或者说他们被造出来就是这个目的)。所以,假如用户提出的需求他们无法满足,他们也不愿意给出零回答或者负面回答(“我不知道”、“我不会”、“不太可能”,等等),而是编造一个答案。在某些情况下,尤其是自然语言交互时,幻想答案可能问题不大;但是在编程领域,幻想答案就完全不可接受了。

    尤其是,如果这些工具无法预先理解数据库结构、也无法判断执行环境,幻想就会频频发生在各种不同的地方,让人防不胜防。

    朋友的想法

    他想在本地跑 Code Llama 模型,实现 text2sql。他打算做一些微调,以便突破我前面说的问题。

    之所以想在本地跑,是希望借助苹果 CPU 内存数量大的架构优势,突破 GPT 的上下文容量限制。

    他不想从 prompt 入手,主要觉得太 low,不够技术。

    我的建议

    我的观点则相反:一定要从 prompt 入手。Prompt 不是玄学,事实证明,合理优化 prompt,可以得到更好的结果。

    其次,不管哪家模型,都已经做好了完整的基础设施建设,基本上不会在生成端遇到问题。开发者只需要关心自己的业务逻辑开发。

    从 text2sql 的角度来看,fine-tuning 当然可以得到最好的结果,但是 fine-tuning 之后,数据库结构就跟模型绑定了。否则的话,一定要想办法把数据库结构解释给 LLM,也就是说,一定要把数据结构放进 prompt。这是基本原理,也意味着我们不需要绕开 prompt engineer。

    总结起来,我的看法是,如果要做这样的产品,应该:

    1. 想办法解析表结构、数据库结构
    2. 把解析结果,准确描述给 LLM(prompt)
    3. 解析用户的要求,尽量还原成数据库结构
    4. 然后一起交给 LLM 来执行

    这里就存在一些难点:

    1. 理解数据结构
    2. 理解用户的需求,和数据结构建立关联
    3. 输出正确的语句,不要出现幻觉
    4. 输出稳定,能够被你的后续处理代码正确处理

    如果模型比较好,可以节省(3)的时间。但是我猜测如果参数少,比如 7b、13b,那么(1)(2)都会很麻烦。所以,公共模型好,还是自己微调的模型好,最终效果不好判断。

    总结

    目前 LLM 尚且无法独立完成工作,应该说是我们开发者的幸运。不过,基于 LLM 开发应用跟以往不同,不太好套用以往的经验,比如上来就自建+调优。

    我建议大家要对 LLM 的基本原理有一些了解,知道 LLM、fine-tuning、embedding+searching 等之类的功能是怎么实现的,能解决什么问题,会有哪些缺陷。这样就能选择最快捷方便有效的方案。

    希望我那个朋友一切顺利。如果各位读者对 LLM 或者应用开发有什么问题,欢迎留言讨论。

  • Prompt Engineering 经验分享

    Prompt Engineering 经验分享

    我这大半年来都在围绕 ChatGPT API 做事,积累了一些 Prompt 相关的经验,大部分跟编程有关。即拿到结果后,我们不直接输出,而是使用代码处理这些结果,然后再输出。Functional Calling 对编程当然有用,不过有时候,配合文中的一些方法,可以得到更好用的结果。

    ChatGPT 的基本原理

    ChatGPT 是一个生成式大语言模型,它由海量的数据训练而来。所以当我们输入一些内容作为启动数据之后,它就会计算出来最可能最合理的新内容。比如,输入“白日依山尽”,那么最合理的接续多半是“黄河入海流”。当然,由于 ChatGPT 已经针对“聊天”这个场景做过优化,所以,他可能会多说一些过渡性的内容。

    通常来说,我们发给 ChatGPT 的内容不会这么好预期,所以它会产出的结果也存在很大变数。这对我们来说有好处也有坏处。好处是,多变的结果,会让我们有更多期待,也更有机会拿到想要的结果;坏处是,结果质量可能忽高忽低,格式也飘忽不定,难以在程序里使用。

    Prompt 入门

    要写出好的、有效的 Prompt,第一步应该去认真阅读 OpenAI 官方的 GPT最佳实践(GPT best practices)。这里我简单总结一下:

    请求里包含必要的信息

    比如我家孩子想让 GPT 帮他写作文,如果只说:“帮我写一篇作文”,效果就不好。因为作文有很多可能,不同的阶段、不同的文体、不同的主题,写出来的作文可能完全不同。

    这时候就要耐心跟 GPT 讲清楚:“我是一名小学五年级的学生,请帮我写一篇作文,大约 300 字,记录我们家过中秋节的故事,我们吃了月饼,看了晚会;我们本来想出去旅游,但是爸爸妈妈猜到处都是人,就没有出去。”

    让 GPT 扮演一个角色

    GPT 已把成千上万的角色融于一身。还是上面的例子,我家孩子如果只让 GPT 帮他写作文,能写,但是未必敢交给老师。所以此时就要让 GPT 扮演同样的小学五年级的学生来写作文。

    给 GPT 提供周边信息的时候,要把边界标清楚

    没有格式的文字,无论是人还是机器都无法理解。所以我们可以使用各种 XML 标记,或者三连引号,让 GPT 知道哪些是我们的请求、哪些是我们给它的参考资料。

    提前帮 GPT 分解任务

    GPT 目前的逻辑能力有限,如果我们有更靠谱的解法,直接教给它会更有效率。

    提供例子让 GPT 参考

    这个策略在二次开发领域会大量使用,我觉得比 functional calling 更常用。

    指定输出内容的长度

    我们知道,语言都存在信息密度,想把一件事情说清楚,可能需要很多文字;而过多的文字,也可能存在一些“废话”。所以限制输出长度往往也可以行之有效地改进结果。

    不过实际上,内容长度会跟很多因素有关,往往不能简单一限了之;如果篇幅限制,实在说不清楚,GPT 也可能会忽略我们的某个要求,大量文字一吐为快。

    我的经验

    对编程来说,稳定性非常重要,因为我们的代码无法适配各种各样千奇百怪的输出。这些输出在 ChatGPT 的聊天界面里,面向使用自然语言的普通人,其表达能力没有问题,但是对我们的程序来说,一些微微的差异也可能破坏代码功能。

    总则:把 GPT 当成态度超好但能力一般的实习生

    GPT 拥有海量的知识,但是缺少足够的逻辑思维能力去组织、架构这些知识。于是我们不能指望 GPT 能够很好的利用这些知识帮我们做事情。更多的时候,我们要先想清楚怎么做,拆解出来步骤,再把任务逐一分配给 GPT,让它尽量简单地做执行工作。

    但是 GPT 态度绝对好,绝对耐心,它可以不厌其烦的反复尝试我们交代的工作,毫无怨言。真是一个能力平平的社畜……

    如果你不知道下一步该怎么做,不妨把 ChatGPT 当成一位无法独立处理工作的实习生,尝试带领它工作,而不是期待他能解决你都不知道该怎么解决的问题。

    减少歧义,尤其是隐含的歧义

    有时候,我们的表达会有一些隐含的歧义。比如,我们去吃饭,想点一份不辣的鱼香肉丝或者回锅肉,这里面就包含歧义——按照川菜里的标准定义,鱼香肉丝和回锅肉都有辣。如果是在不常吃辣的地方,厨师可能可以试一试;如果实在巴蜀本地,那多半厨师要谢绝接待了。

    我们向 ChatGPT 提要求的时候也要注意。举个例子,我厂的产品会要求 ChatGPT 帮忙写一封信,这封信需要遵循一定格式,我们才好解析它并重新格式化。但是我们发现,GPT 在写开头(intro)的时候,经常会只写:Dear Meathill,即问候语(greetings),然后漏掉我们希望有的第一段。反复换模型也没有效果。后来我把要求改成

    intro: greetings, then one paragraph of introduction about 50 words,终于解决了问题。

    因为对于 GPT 来说,一句 greeting 也可以是 intro,只要求写 intro,它搞不清我们的目的,输出就远不如后面准确。

    使用 YAML 传递格式化数据

    JSON 格式要求很严格,很容易出错,而且在得到完整结果前,也很难解析。所以我建议大家如果需要格式化数据,不要用 JSON,用 YAML。YAML 格式更简单,不容易出错;而且 YAML 在流式传播的时候,不耽误我们实时解析并且输出,效果更好。

    比如这样:

    Please, as a Christian minister, help me choose a thought-provoking verse from the Bible, tell me why you chose it, and then write a prayer for me. Please write to me in the following YAML formats. No other content.

    “`yaml
    verse: the verse content
    reference: the verse you select for me
    thought: teach me about this verse, about 80 words
    prayer: use it to lead me to prayer, about 80 words
    “`

    控制 Prompt 的长度

    正如前面所说:

    1. ChatGPT 的推理能力并没有传说中那么强;
    2. 自然语言里难免会存在前后矛盾之处

    所以过长的 prompt 很容易导致得到不稳定、不可靠的结果。网上能找到各种洋洋洒洒一大篇的超长 Prompt,实际上以我的经验,这些 Prompt 要么实际效果一般,要么有许多限制条件并不必要。尤其是那些限制 ChatGPT 应该说这个不应该说那个的,多半因为前后矛盾实际上并未生效。

    我建议大家保证遵守上面最佳实践的六点之后,尽量用简短无歧义的语言提出要求,得到的结果会更加可靠。

    (案例待补充)

    Embedding + Searching 中文一般,英文略好

    经我们测试,中文 Embedding 的结果差强人意,检索匹配度很差,感觉跟传统关键词搜索的效果差不多,自然语言与原文表达相似的意思,但是词汇完全不同的时候,经常搜不出结果。

    英文略好一些,不过也好不到哪儿去,事实搜索强于表意搜索,做知识库知识管理的话,问题不大;期待做回复系统的话,我认为并不可行。实际上,我体验那些所谓名人聊天工具时,感觉也是如此。

    比如,类似 trickle.com 这样的知识管理工具,存进去一些统计数据,如股票价格、销量等,然后基于自然语言进行检索:“苹果股价最高时是多少?”一般来说没有问题。但是如果写日记,然后搜索:“我那天特别开心,是怎么了来着?”,就基本没有结果。

    解决方案当然也是有的,在 Embedding 存入数据库时,预设一些搜索场景,然后让 ChatGPT 帮助生成搜索辅助内容,最后一起 Embedding 存入数据库,这样搜索的时候就有更大概率能找到。比如:

    这是我的日记,请分析我日记中所表述的心情、印象、态度,概括为 10~20 个形容词。请只用 TypeScript `string[]` 的格式输出。No more other content.

    “””长假期间天气好热,想出去玩,但想到这么热人又多就懒得走了……”””。

    总结

    ChatGPT 非常强,但要让他发挥全部战力,我们开发者的努力也不可或缺。以上是我这几个月来学习总结得到的经验,希望对大家有用。也期待看到更多开发者从编程角度,分享二次开发的经验。

  • 上传文件到 CloudFlare R2

    上传文件到 CloudFlare R2

    上周日突然感染流感,发烧一天多,头昏脑涨两三天,只好请假休养。不知道是不是二阳,懒得测。工作进展不多,周末适当加加班,博客适当划划水,这周就记录一下 CloudFlare R2 上传文件吧,实测效果很好,简单省事好用,推荐大家使用。

    CloudFlare R2 兼容 AWS S3 API,但是不需要那么复杂的 IAM 体系,而且直接对接 CloudFlare CDN,我觉得更好用。跟国内的众多云存储比起来,它不需要备案,还提供域名和证书,用起来更简单。所以,如果你的网站或者服务需要一个云存储,但是暂时不想负担太高的成本,我建议先试试 CF R2。

    我假设读者已经拥有 CF 账号,在 R2 里创建了 bucket,并且完成了需要的配置(好像只需要配置一个域名)。接下来,我们需要在自己的业务代码里实现上传。

    首先,我们要安装 AWS SDK。

    pnpm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

    接下来,我们需要在项目中建立 S3 配置,其中关键信息都写在 .env 里。

    import { S3Client } from '@aws-sdk/client-s3';
    
    const S3 = new S3Client({
      region: 'auto',
      endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
      credentials: {
        accessKeyId: process.env.R2_ACCESS_KEY,
        secretAccessKey: process.env.R2_SECRET_KEY,
      },
    });
    
    export { S3 };

    然后就可以生成预签字的上传路径,相当于提前校验用户身份,这一步需要放在服务器端进行,避免泄漏关键信息。我这里因为业务需求比较简单,没有做复杂的鉴权和检查,如果有需要,就多验证几步。

    import { PutObjectCommand } from '@aws-sdk/client-s3';
    import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
    import slugify from 'slugify';
    import { H3Event } from 'h3';
    import { S3 } from '~/lib/s3';
    import { ApiResponse, PreSignedUrl } from '~/types';
    
    export default defineEventHandler(async function (event: H3Event): Promise<ApiResponse<PreSignedUrl>> {
      // 这一步提交的是预处理信息,没有文件本身,所以还是 json
      const json = await readBody(event) as { fileName: string | undefined, fileType: string | undefined };
      const { fileName, fileType } = json;
    
      // 生成存储对象 key,其实就是文件名
      const objectKey = `${slugify(Date.now().toString())}-${slugify(fileName)}`;
    
      // 生成预签名 url
      const preSignedUrl = await getSignedUrl(S3, new PutObjectCommand({
        Bucket: process.env.PUBLIC_S3_BUCKET_NAME,
        Key: objectKey,
        ContentType: fileType,
        ACL: 'public-read',
      }), {
        // 此 URL 5分钟内有效
        expiresIn: 60 * 5, // 5 minutes
      });
    
      return {
        code: 0,
        data: {
          preSignedUrl,
          objectKey,
        },
      };
    });

    最后,我们就可以上传文件了。

      const response = await $fetch<ApiResponse<PreSignedUrl>>('/api/get-upload-url', {
        method: 'POST',
        body: {
          fileName: file.name,
          fileType: file.type,
        },
      });  
      const { preSignedUrl, objectKey } = response.data as PreSignedUrl;
      const uploadToR2Response = await fetch(preSignedUrl, {
        method: 'PUT',
        headers: {
          'Content-Type': file.type,
        },
        body: file,
      });
      if (!uploadToR2Response.ok) {
        console.error('Failed to upload file to R2');
      }

    整个上传过程比较简单,服务器只需要做一些权限校验,剩下来的过程都可以交给 R2。关键是,这样一来,因为不涉及文件操作,就可以放心交给 Serverless 来做,Vercel 友好。上传后的文件自动进入 CF CDN,也很容易使用。

    学会使用 R2 之后没多久,我的 Vercel Blob 存储的申请也通过了。必须得说,这些互联网基础设施对我们全栈开发者来说非常重要,而无障碍的域名对目前在国内还是一种奢望,所以用国外服务就成为一种必然。希望未来会更好吧。

  • 使用 CloudFlare 突破图片防盗链

    使用 CloudFlare 突破图片防盗链

    好久没更了,最近遇到很多挑战,比如使用 Flutter 开发 App。打个岔,我发现 Cursor.sh 真是个好东西,面对 Flutter 这种我完全不熟悉的语言,Cursor 提供了很方便的 AI 辅助开发,包括一系列很好用的功能:

    1. Prompt => 代码
    2. 问题解释
    3. 结合上下生成代码或提供建议

    如果是以前,我需要先看文档,学习 Get Started,然后慢慢一行一行摸索。现在我只要告诉 ChatGPT 我需要什么样的代码,它就能自动帮我升成一堆能用的代码,虽然不够好,但是比我自己慢慢搞效率高多了。

    推荐各位使用。在面对自己不熟悉的语言、框架时,真的能大幅度提升效率。

    回到正题。

    前几天遇到一个问题。有个朋友要抓某个网站上的数据做电商网站,其中有很多图片。因为网站还没有上线,也不知道能不能挣到钱,所以想先降本,尽量不要先把钱花出去。

    所以我就想,先不把图片都抓下来放自己的服务器上,直接使用对方的地址(盗链)行不行?测试一下,果然不行,有防盗链。然后我就想到 Serverless,感觉这是个很适合的场景:

    1. 先把源图片抓下来
    2. 进行一些处理,比如压缩、切割
    3. 然后返回一个新的图片

    对于我们来说,只是不需要第二步而已,那就这么做吧。接下来就是选平台,其实我一般会优先用 Vercel,毕竟现在我大部分项目都在上面跑着。不过考虑到平时推荐 CloudFlare worder 的人也很多,他们家的免费方案也很慷慨,所以我想这次试试 CF 吧。

    没想到 CF 的文档非常全,直接就提供了类似的用例教程:Custom Domain with Images · Cloudflare Workers docs,于是我就照猫画虎,搞定了这个需求。代码很简单,我觉得都不需要详细解释:

    export default {
      async fetch(request) {
        // 解析图片地址,取出 pathname 部分
        const { pathname } = new URL(request.url);
    
        // 从原始地址获取图片,直接返回即可
        return fetch(`https://src-domain.com/${pathname}`);
      },
    };

    接下来的缓存等功能都是齐备的,不需要每次都返回源站。至于墙的问题,自己的域名 CNAME 过去,也是可以直接使用的。

    CF 的还有很多教程和范例,建议大家抽空看看:Examples · Cloudflare Workers docs


    总结

    功能不大,开发体验很好,从查找文档到完成上线,总共可能花了一个小时多点。有流量之前都不用考虑费用问题。省心省力。以后准备多多用起来。

  • Bug求助+悬赏

    Bug求助+悬赏

    哎,老革命遇到新问题,创业+远程工作之后,想拉几个人跟我一起排查 bug 也找不到,只好公开求助+悬赏。

    首先请大家看下面这段视频:

    故障描述

    简单来说,我们做了一个网站:Dailylift.io。在这个网站上,你能描述自己今天的状态,然后得到一些心灵慰藉:你信仰的目标会给你写一封信。

    这个功能并不复杂,我选择用 Nuxt3 + Supabase 作为技术栈。我的理由是:

    1. 因为我们需要 SEO,我希望利用 Nuxt3 SSR 功能提升 SEO 效果。
    2. Supabase 提供包括开箱即用的用户系统在内的一系列 Serverless 功能,可以帮助我们的开发提速。
    3. Supabase 提供 Nuxt3 SDK,比 Auth0 更方便。
    4. Supabase 提供 VectorPG,方便我们使用 Embedding

    但是实际运行起来之后,却发现一些我未曾预料到的问题,主要跟用户登录状态有关。具体复现步骤为:

    1. 用户未登录时,先填写感受和目标,然后点“收信”,我们会要求他登录
    2. 用户登录后,我们需要先检查他的收信记录,确保每天只有收一封信
    3. 接下来,如果他今天还没有收信,我们就继续之前的过程,给他写信
    4. 但是偶尔,前面都正常,登录之后就卡住了,无法进入发信流程

    我尝试了很多方案,想解决这个问题,每次我都觉得自己搞定了,我甚至还写了一篇博客作为分享:使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录。但是之后老板就会再次遇到这个问题,然后把我一通数落。我怎么也找不到稳定重现的方式,只能反复查验代码,希望找到其中的漏洞。

    我有几个怀疑:

    1. Nuxt + Supabase,理论上存在两个环境有用户态:SSR与浏览器。我不确定这里会不会有问题。比如,为了防止 refresh token 的时候触发 onAuthStateChange,我用 useSupabaseUser 获取初始用户状态。会不会是它导致后面判断出错了(很小概率)?
    2. 我需要先检查用户的邮件历史,再决定是否请求信件。这里可能也会有问题,比如两次请求之间的先后顺序。
    3. Supabase SSO 只支持 redirect,必须跳走再回来,所以浏览器里的状态无法保存。从第三方回来,处理 access_token 期间,可能因为网络状态、本地内存状态等,进入到某个我没处理的分支,导致失败。
    4. 登录之后,onAuthStateChange 到用户登录彻底完成之前,中间其实有一小段时间,会不会是网络问题导致它被卡住了?

    总之吧,我现在一个人开发,面对这些问题有些焦头烂额,所以希望所有看到这篇博客的同学,能帮我测试一下网站:Dailylift.io,帮我想想其中可能存在的坑,如果您有处理类似问题的经验,也请多分享一些。无论您:

    1. 帮我找到稳定重现的方式
    2. 帮我固定住 bug 出现的现场
    3. 帮我找到代码中潜藏的问题
    4. 引导我找到正确的方向
    5. 分享有价值的经验

    我一定大红包伺候!感谢大家,期待帮忙。


    更新:

    2023-08-31

    今天我早上测试的时候,遇到一个很诡异的问题:我通过 Google 登录后,遇到一个加载信件列表 400 错误,导致获取新信件失败。

    看路径和参数,这个请求应该是正常的。最诡异的是,我在 Devtools 里看不到这个错误,只能看到两个 GET /letters 的请求,都是正常 200。

    我现在怀疑这个问题是不是这样的:

    1. SSR 阶段它也会发起一个请求
    2. 这个请求在某个情况下会失败
    3. 这个失败会导致渲染出的页面携带着失败的状态,继而无法继续。

    另外,因为我们使用 TiDB Serverless,会不会是冷启动时会产生长延迟,导致 Vercel Serverless 失败?不过我看了数据库,也有很多是几个小时后成功的,也不太像。

  • 应用创意:AI 求职助手

    应用创意:AI 求职助手

    好久没发应用创意。我之前想做一个 AI 简历工具,琢磨来琢磨去,我觉得只关注简历是不对的,太狭隘。简历本身价值很有限,作用也不太大,用户关注简历的时间很短,做简历很难做成一个长期项目,用户大概率会用完就走,也不太容易会付费。

    所以必须升级成 AI 求职助手,才有做的必要。这里把我的想法放出来跟大家交流一下。

    首先,我们来想一想,在求职中,目前通行的各环节作用是什么?我的观点是:

    1. 简历,证明我适合这个岗位。
    2. 面试,证明我的简历所言非虚。
    3. 上家公司的薪资,证明我值得这份钱。

    具体的分析可以参见 从招聘方的角度理解求职,本文不再详叙。

    有些同学不会写简历,我认为一方面因为不理解简历的作用,另一方面,现阶段各种写简历的工具过分关注于简历本身,而缺少对于写好简历的教育。

    另外,写好简历只是第一步,后面还有面试和入职,现在也缺少相应的工具来帮助用户。其实“刷题”这个需求已经被满足得很好了,但是“模拟面试”的工具,感觉还不太够。

    再及,因为我自己是远程工作,所以我比较关注这个领域。我发现很多自由职业者、独立开发者,其实也有做简历的需要。未来可能会有很多开发者不仅仅面向一份全职全时工作,而是同时在多个领域多个方向做开发,那么同时维护多份简历也很常见。

    所以我想创建这样一款工具:

    1. 不要求用户一次性完成一份简历,而是鼓励用户不断添加自己的工作履历和项目经验。
    2. 根据一些既定规则,判断一段工作履历和项目经验的描述是否合格、有何改进空间,提升每段经历的表达质量。
    3. 当我决定投递简历的时候,AI 求职助手会根据招聘目标、JD,帮我自动生成一份高质量的简历。
    4. 如果我的经历经验跟目标岗位要求不符,它会提醒我,还会告诉我应该补足哪些的知识。
    5. 它还可以针对目标岗位,给出模拟面试题,我做完面试题之后,它会给我打分。
    6. 面试之后,我们可以反馈面试结果,让 AI 求职助手帮我们判断面试表现,复盘整体表现,提升下次面试的表现。
    7. 它还可以主动出击,去全网搜集适合我的 JD,主动帮我们生成、投递简历,帮我们准备面试,帮我们复盘,等等。

    短期内这样的面试助手只会针对 Web 前端、全栈这样我最熟悉的岗位,未来可以的话,再慢慢扩展到更多的岗位(需要其它岗位上经验丰富的同学来充当内容编辑)。

    我希望这样的产品是订阅制的,按月计费。考虑到可能需要使用 GPT-4,我觉得定价太低也不行,估计得在 100~200 这个区间吧。不知道各位读者有什么意见、建议,欢迎在评论里告诉我。

    希望大家都能找到满意的工作。