基于OpenAI对个人博客文章全量摘要汇总开发过程记录

AI 摘要: 本文介绍了通过OpenAI辅助研发、阅读、翻译、编程等场景的经验,以及使用OpenAI进行SWOT分析和获取解决方案的方法。同时还提到了几款个人常用的AI辅助工具。

1. 背景

之前的 Blog 都是基于 Hugo(一款静态站点生成软件)生成,内容主要为 Markdown,同时每个 MD 的顶部都有文章标题、关键字、分类、创建时间等信息(基于 Yaml 格式配置),格式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
title: "基于OpenAI对博客文章进行Summary总结开发记录"
date: 2023-08-21T16:35:42+08:00
description: "Blog Summary Application Development"
keywords: "Blog Summary Application Development"
categories:
  - APPLICATION
tags:
  - AI
  - OpenAI
type: posts
draft: true
include_toc: true
weight: 100

我遇到了两个问题:

  1. 一是很多 Blog 在写完后,并未对其进行摘要介绍,导致在首页展示的时候,都是空空如也(可以通过 Hugo 进行一些配置截取指定的部分),导致用户无法快速对 Blog 文章进行了解;
  2. 另外一点是关键字 SEO 搜索优化,之前的文章都没有关注这个关键词的问题,导致很多文章关键字和内容没有那么契合;

遇到两个问题后,可以一篇一篇修改,不过改动工作量很大,有没有简易一些方案?

从 OpenAI 出来后,就一直在使用 OpenAI 的接口做知识辅助(之前有介绍我是如何打造我的 OpenAI 研发辅助环境的,参见: https://tkstorm.com/ln/chatgpt-tips。 到此就引入了今天的主题,如何基于 OpenAI 对 Blog 文章进行总结,并自动替换掉 Hugo 的头部 Yaml 内容,集合 Hugo 的 Index 的模板修改后,就变成了下面的结果样子:

PS. 中间有一些 Go 基础代码、SQLite 的使用,不感兴趣可以直接跳过

1.1. 软件基础功能述求

项目动工前,结合自己的产品上的想法,梳理了下软件的基本功能:

  1. 递归处理整个目录下的 MD(为了测试需要,要支持传指定路径 MD 也可以)
  2. 代理网络支持,应用要能够走网络代理访问 OpenAI(支持应用配置网络)
  3. 能够动态配置 prompt,优化内容的 Summary 质量(通过文本配置化形式实现)
  4. 为了逐个文章做内容替换,要用到 Go 的正则、文本处理相关库,同时避免重复内容,需要记录并存已处理过的内容(通过文本或 DB 存储已处理过的内容)
  5. 考虑到整个目录有 200 多 MD,需要并行处理,提升处理效率
  6. 考虑软件的扩展(不单单只有 AISummary),考虑分层设计(选用 DDD 目录布局分层,这个布局是结合过往经验得到,参考即可)

大体的想法就如上,下面记录下一些软件的开发过程记录。

1.2. 整体流程

整体流程分成三大块:

  1. 项目入库部分,基本是参数的传入、配置文件读取
  2. 针对传入的文件或路径进行遍历,提取全量的*.md文档,并行进行具体任务执行
  3. 针对具体每个 Blog 文档进行并发处理(提取、OpenAI 接口调用、替换)

2. 项目实施日志

2.1. OpenAI 接口测试 - RapidAPI 测试

先简单的在 RapidAPI 上调通 OpenAI 的请求接口,如果存在网络问题,参考文章解决:https://tkstorm.com/ln/cloudflare-warp

RapidAPI: Rapid API 是一个方便易用的平台(之前名字为 PAW),为开发人员提供了快速访问和集成各种 API 所需的工具和资源,(我 MACOS 开发一直都是用的这款,一次买断很划算,很早以前就从 postman 转 RapidAPI,强烈推荐!)

RapidAPI 主要是用于 OpenAI 的 Restful 接口测试和快速验证:

2.2. 仓库代码说明

仓库地址: https://github.com/lupguo/copilot_develop

目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ tree -L 2
.
├── README.md
├── app // 基于DDD代码也做了下分层
│   ├── application // 应用层:业务流编排(组装多个Domain下的服务流,负责缓存、并发处理流)
│   ├── domain // 业务实现(Blog做内容汇总&替换)
│   ├── infras // 基础设施(RESTful、DB、配置等)
│   └── interfaces // 接口层(参数检测、错误映射)
├── cmd
│   └── blog_summary // blog summary启动程序
├── conf
│   ├── app_dev.yaml
│   ├── app_prod.yaml
│   └── prompt.yaml
├── data // SQLite本地存储
├── deploy // 部署发布相关
├── go.mod
├── go.sum
├── main.go // 这个是copilot_develop服务的程序,可以先忽略
└── test // 测试相关

补充说明:

  1. BlogSummary 只是 Coplilot_Develop 项目的一个开始,后续cmd目录下考虑到会添加更多其他的一些功能,所以这里暂不使用最顶层的main.go实现
  2. 关于 DDD 的文章讨论: https://tkstorm.com/posts/ddd-layout

2.3. Go - 并发相关(扇入扇出、资源泄露)

并发实际是一个比较大的话题,顺带复习了下以前看过的并发相关内容,简要记录了下,相关内容参考如下:

  1. https://www.youtube.com/watch?v=f6kdp27TYZs
  2. https://go.dev/blog/pipelines

一些相关知识点内容:

2.3.1. Fan-out、Fan-in、通道未关闭、Goroutine 资源泄露问题

  • 关闭通道与 panic 问题: 向关闭的通道上发送会出现恐慌 panic,所有可以通过 sync.WaigGroup来达到并发的同步: wg.Add(len), wg.Done(), wg.Wait()

  • Pipeline 模式:通过 Chan 通道连接不同的处理流程,每个流程的处理是一组相同的 Goroutines 执行,这些 Goroutine 会从上游接收值 → 数据处理 → 输出给到下游接收 Chan

  • Fan-out,Fan-in:通过 Chan 做扇入和扇出,扇入后通过 range

    • Fan-out(扇出):指一个进程或任务将其输出分发给多个子进程或子任务的过程(常见一个 RPC 调用,会分拆到多个基础 RPC 接口调用)。在并行计算中,当一个进程产生多个输出时,这些输出可以被同时发送到不同的处理单元进行处理。
    • Fan-in(扇入):指多个进程或任务将它们各自的输出合并为一个结果的过程。在并行计算中,当多个处理单元完成各自独立工作后,它们可以将结果传递给另一个进程或任务进行汇总和整合。Fan-in 通常用于描述数据流图中收集结果的节点。

2.3.2. 资源泄露问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func merge(cs ...<-chan int) <-chan int {
		// 利用sync.WaigGroup确保在close(out)关闭一定发生在所有接收通道cs关闭后执行,避免了往关闭通道上发生引发的panic问题
    var wg sync.WaitGroup
    out := make(chan int)

    // Start an output goroutine for each input channel in cs.  output
    // copies values from c to out until c is closed, then calls wg.Done.
    output := func(c <-chan int) {
        for n := range c {
            out <- n
        }
        wg.Done()
    }

    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }

    // Start a goroutine to close out once all the output goroutines are
    // done.  This must start after the wg.Add call.
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

上述代码存在问题: 扇入处理的函数通过 range 不断读取 chan 内数据,直到该 chan 关闭,但可能存在上游 Goroutine 关闭 chan 的失败或异常的情况,导致 chan 通道未关闭,以至于扇入的程序无限期阻塞(上面的示例,如果 cs 中存在一个 chan 一直未关闭,那么 wg.Done 就无法执行,最终 merge 函数会一直阻塞),如果 merge 执行特别多次,就引发了资源泄露(描述符未做回收、Goroutine 的申请的资源未释放、变量因为还存在应用也导致无法被 GC 回收)

如何解决?:

  1. 通过配置有缓存区的 chan,不带缓冲的 chan → 带缓冲的chan,不过缓冲 size 大小不好确定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // 缓冲区chan,避免异常导致chan未能正常关闭
    func gen(nums ...int) <-chan int {
        out := make(chan int, len(nums))
        for _, n := range nums {
            out <- n
        }
        close(out)
        return out
    }
    
  2. 采用 range 通道时候,通过 select 从接收通道检测是否通道已关闭,如果已关闭则直接退出。因为从已关闭的通道上的接收操作,总是可以立即进行,并得到产生元素类型的零值。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    
    // 通过信号明确取消 - suggest
    func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
        var wg sync.WaitGroup
        out := make(chan int)
    
        // Start an output goroutine for each input channel in cs.  output
        // copies values from c to out until c is closed or it receives a value
        // from done, then output calls wg.Done.
            // 扇入合并多个输入chan,扇入到out chan中,如果遇到任何意外(上游c没有正常关闭,导致range一直阻塞),output协程通过从<-done chan内收到结束信息,也会让out chan正常关闭
        output := func(c <-chan int) {
                    defer wg.Done()
            for n := range c {
                select {
                case out <- n:
                            // 防止c
                case <-done:
                                return
                }
            }
        }
    
        wg.Add(len(cs))
        for _, c := range cs {
            go output(c)
        }
    
        // Start a goroutine to close out once all the output goroutines are
        // done.  This must start after the wg.Add call.
        go func() {
            wg.Wait()
            close(out)
        }()
        return out
    }
    
    func main() {
        // 通过退出的done信号,可以广播给到所有的消费chan处理协程,
        done := make(chan struct{})
        defer close(done)
    
        in := gen(done, 2, 3)
    
        // Distribute the sq work across two goroutines that both read from in.
        c1 := sq(done, in)
        c2 := sq(done, in)
    
        // Consume the first value from output.
        out := merge(done, c1, c2)
        fmt.Println(<-out) // 4 or 9
    
        // done will be closed by the deferred call.
    }
    

2.3.3. 并发度控制,避免过多 Goroutine 处于等待状态

如果并发数量不加以限定,可能导致同时启动大量 Goroutine,即便 Goroutine 本身占用的资源小,但架不住大量的 Goroutine 因为获取不到 CPU 的资源,而处于等待状态,占用着系统资源

  1. 基于 sync.WaigGroup{}结合 buffer chan 做信号量控制并发的度
  2. 直接利用 errgroup.Group 包,实际就是对上述流程的封装,但足够简单(推荐使用)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 方式1: 通过原生的WaitGroup做同步,以及有buffer的Chan做信号量控制并发
func TestWaitSema(t *testing.T) {
	// 通过sema信号量控制并发,通过wg进行同步
	sema := make(chan struct{}, 2)
	wg := &sync.WaitGroup{}
	for i := 0; i < 100; i++ {
		wg.Add(1)
		sema <- struct{}{} // 注意这里的seam信号是需要再for内,而不是goroutine内,避免同时启动多个Goroutine的问题
		go func(i int) {
			defer func() {
				<-sema
				wg.Done()
			}()
			t.Logf("goroutine #%d", i)
			time.Sleep(time.Second)
		}(i) // 注意i要传值
	}
	wg.Wait()
	t.Log("Done")
}

// 方式2:通过errgroup包,简化并发控制
func TestErrorGroup(t *testing.T) {
	// errgroup 控制并发
	egp := errgroup.Group{}
	egp.SetLimit(2)
	for i := 0; i < 100; i++ {
		i = i
		egp.Go(func() error {
			t.Logf("goroutine #%d", i)
			time.Sleep(time.Second)
			return nil
		})
	}

	if err := egp.Wait(); err != nil {
		t.Errorf("egp got err: %s", err)
	}

	t.Log("Done")
}

2.3.4. 当前 Go 进程运行的协程数量打印

可以定期打印runtime.NumGoroutine()数量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func asyncShowGoroutineNum(t *testing.T, over chan struct{}, interval time.Duration) {
	go func() {
		for {
			select {
			case <-time.Tick(interval):
				t.Logf("goroutine cnt(%d)", runtime.NumGoroutine())
			case <-over:
				t.Logf("exist test")
				return
			}
		}
	}()
}

2.3.5. 管道使用指导原则

  1. 当所有发送操作完成,关闭通道 chan。管道中的每个阶段可能会阻止尝试向下游发送值,并且下游阶段可能不再关心传入的数据,关闭通道操作向管道启动的所有 goroutine 广播“完成”信号
  2. stages 任务处理阶段保持不断从入口通道接收值,直到这些入口通道关闭(close(ch))或者入口通道被解除阻塞(通过select监听到指定信令后return退出)(参考 output 函数)
  3. 通道 chan 通过增加 buffer 缓冲区容纳更多发送值,或者通过明确终止信号解除对方的阻塞

2.4. Go - 文件处理、字符处理相关

下面记录的是在开发过程中,涉及 Golang 标准库中 IO 相关操作

2.4.1. 文件 IO 读写操作

  1. 文件读写操作,os.Open()和 os.OpenFile()区别
    • 前者以只读+创建模式打开,后者可以支持写入、清空等其他多种模式处理
    • os.Open()在文件或路径不存在时候会报错: no such file or directory
    • os.OpenFile(md.Filepath, os.O_TRUNC|os.O_WRONLY, 0644) 这里的 os.Flag 需要注意: O_TRUNC表示清空文件后写入、O_WRONLY只写模式(还有O_RDONLY只读模式)、还有O_CREATE(文件不存在则创建)、O_SYNC(同步写入,默认写入文件缓存)、O_APPEND(增量写入,默认不加的话,从文件最开始位置写入)
  2. filepath.Join()操作,做路径拼接
  3. 临时文件生成
    • os.TempDir()
    • tempFile, err := os.CreateTemp("", "exist_*.md") 创建临时文件
    • 注意:临时文件最后要程序自己清理
  4. 清空文件方法:
    • os.Truncate()系统调用、利用 os.OpenFile()自定义系统 flag - os.TRUNC 标识、文件可以写下打开模式
  5. 打开的文件句柄 file 支持通过file.Name() 获取路径名称
  6. file.Close()操作,防止资源泄露:
    • 避免资源浪费,GO 的 GC 会自动回收(不过时间太长了),如果存在未关闭的 FD 过大,GC 压力将增大,同时 FD 的受操作系统的应用进程的 ulimit 限制,可能导致 FD 泄露应用进程无法打开更多的 FD 导致程序异常
  7. 如何检测文件是否为普通常规文件: 通过 os.Stat(filename) 获取文件信息,然后通过文件的 file.Mode()检测是否为常规文件
    • 通过 fileInfo, err := os.Stat(filePath) → fileInfo.Mode().IsRegular() 检测
  8. 如何获取文件的当前路径:os.Getwd() 操作

2.4.2. 字符串操作

如何从一个字符串中,截取到指定字符串的位置?

可以通过 idx :=strings.Index(s,sub) 检测是否 sub 在 s 中,不在返回 -1 ,然后通过 s[:idx] 返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// GetAppRoot 获取项目根目录
func GetAppRoot() string {
	dir, err := os.Getwd()
	if err != nil {
		return ""
	}
	index := strings.Index(dir, "app/application")

	if index > 0 {
		return dir[:index]
	}
	return dir
}

2.4.3. bytes.Buffer 对象与 bufio 缓冲库

字节缓存库bytes.Buffer和 bufio 缓冲库很类似,后者支持将一个 io.Writer 或 io.Reader 库包装成缓冲库,支持最后 w.Flush()操作,可以减少系统调用次数

2.4.4. 程序执行耗时

1
2
3
4
5
6
7
8
9
// 程序执行时间
func (){
	start := time.Now()
	// 注意: 这里需要有个匿名函数包裹,不能直接用 defer t.Log(time.Since(start))
	defer func() {
		t.Log(time.Since(start))
	}()
	...
}

2.4.5. 标准输入输出接口

/dev/null 是 linux 中的特殊文件,可以通过 os.OpenFile("/dev/null", os.O_WRONLY, 0666) 打开黑洞文件,将 oldStdout := os.Stdout 保留后,将标准输出更换到黑洞文件 os.Stdout = devNul,执行完后恢复 os.Stdout = oldStdout

1
2
3
4
5
6
7
8
// 将标准输出重定向到/dev/null:
command > /dev/null

// 将标准错误重定向到/dev/null:
command 2> /dev/null

// 将标准输出和标准错误都重定向到/dev/null:
command > /dev/null 2>&1

2.5. Go - 正则处理相关

正则语法参考: https://github.com/google/re2/wiki/Syntax

这里在匹配 Hugo Blog MD 文档的 yaml 头的多行内容时候(参考文章开头格式),使用正则var blogMdRegex = regexp.MustCompile("(?sm)^---\n(.+?)\n---(?:\n+)(.*)$")

相关说明如下(第一个版本有 BUG,参考注释)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 第一个版本 - (?sm)表示该括号不捕获子表达式、flag的s表示.符号匹配\n(默认不匹配\n), flag的m表示支持跨多行匹配(非单行)
var blogMdRegex = regexp.MustCompile("(?sm)^---\n(.+)\n---\n(.*)$")

// 第二个版本 - 主要变化是(.+?),这里的?表示非贪婪匹配,因为在有些MD中,会存在---隔行符号,导致yaml头部匹配失败
var blogMdRegex = regexp.MustCompile("(?sm)^---\n(.+?)\n---(?:\n+)(.*)$")

// 函数内通过FindStringSubmatch找到匹配的内容,这里因为在正则描述符里面强调一些不捕获,所以match[0]表示整个串、matchs[1]表示yaml头、matchs2[2]表示内容部分
match := blogMdRegex.FindStringSubmatch(string(fileContent))
if len(match) != 3 {
	return nil, fmt.Errorf("blog content yaml header not found")
}

2.5.1. 正则非贪婪匹配示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 输出:
// regexp_test.go:47: Header: header
// regexp_test.go:48: Content: content1---content2
func Test_YamlHeaderGet(t *testing.T) {
	str := "---header---content1---content2"

	// (.*?):使用非贪婪模式匹配任意字符,直到遇到下一个换行符。
	re := regexp.MustCompile(`---(.*?)---(.*)`)
	matches := re.FindStringSubmatch(str)

	if len(matches) >= 2 {
		header := matches[1]
		content := matches[2]
		t.Log("Header:", header)
		t.Log("Content:", content)
	}
}

2.6. Go - 接口 Mock

以前是用的 uber 的 gomock 库: https://github.com/golang/mock,不过现在 Github 上面已归档了处于不再维护状态。

所以,我这里还是推荐改用 https://github.com/stretchr/testify 的 mock 库,基本流程也比较简单:

  1. 首先定义要 mock 的结构体(比如 DB、RPC、API 基础设施),嵌入mock.Mock
  2. mock 结构体实现接口方法,返回值依据后续传入的 args 参数进行断言(参见示例)
  3. 在用例中,实例化结构体,定于结构体要 Mock 的方法返回(参见示例)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 计划mock出一个srv:因为UT要用到基础服务,所以这里直接mock处一个
type mockAISrv struct {
	mock.Mock
}

// 2. 实现结构体方法,该结构体实现了srv服务接口
func (m *mockAISrv) BlogSummary(ctx context.Context, content string) (summary *entity.BlogSummary, err error) {
	args := m.Called(ctx, content)

	// 这里的返回值很考究,依据第三步中的Return(arg0, arg1) 约定,args为自定义mockAISrv.On().Return()的返回参数&entity.BlogSummary{},nil内容,分别对应的args[0]和args[1]
    // 注意:可以通过index指定
	return args[0].(*entity.BlogSummary), args.Error(1)
}

// 3. 这部分代码是用例调用,进行方法调用监听(方法被调用时候触发),按预期返回指定返回值
// 这里的&entity.BlogSummary{}返回值,和nil会作为参数传递给到mock方法的 BlogSummary()中,通过args的Index进行类型断言返回处理
mockAISrv := new(mockAISrv) // 初始mock实例
ctx := context.Background() // 入参
mockAISrv.On("BlogSummary", ctx, mock.Anything).Return(&entity.BlogSummary{
		Keywords:    "Mock Keyword1, Mock Keyword2",
		Summary:     "Mock summary...",
		Description: "Mock Description...",
}, nil)

3. 数据存储

3.1. 为什么选用 SQLite

  1. 一开始想法是通过 file 文件来记录哪些 md 的文件已被更新何时更新,但会存在一些细节处理问题,比如文本之间的分割、特殊字符串处理等(考虑到后续copilot_develp进一步迭代,进行一些其他更加复杂一些数据处理,还是用数据库存储更为方便),初步考虑下来,文件存在还是不如 DB 的结构化好维护
  2. 存储到 DB 又不想使用远程数据库,那么就优先考虑本地数据库了,调研下来SQLite是不错的选择
  3. 另外,在 Golang 中使用SQLite+GORM是很成熟的技术,同时选型SQLite数据库又是本地数据库支持,非常简便,即便后续要替换成类似MYSQL之类也比较容易,几乎不用动什么底层代码

综上几点,最终选用了SQLite+GORM

3.2. 如何使用 SQLite

虽然很久以前用过 SQLite,但还是如何操作基本都忘记了,不过我们不是有 OpenAI 工具么。

结合之前 Mysql 和 GORM 的操作,所以,很快就把 SQLite 的基础设施部分开发完了,一些图示:

3.2.1. SQLite 简要介绍

SQLite 是一种轻量级的关系型数据库管理系统(DBMS),它被设计为嵌入式数据库,可以直接嵌入到应用程序中使用。以下是关于 SQLite 的基本情况的介绍:

SQLite 简介:

SQLite 由 D. Richard Hipp 于 2000 年创建,它是一个开源项目,使用 C 语言编写。SQLite 的目标是提供一个零配置、零管理的数据库引擎,它不需要一个独立的服务器进程,而是直接访问存储在磁盘上的数据库文件。由于其简单性和高效性,SQLite 已经成为广泛使用的数据库解决方案。

应用场景: SQLite 适用于许多不同的应用场景,特别是那些需要在本地设备上存储和访问数据的应用程序。以下是一些 SQLite 常见的使用场景:

  1. 移动应用程序:由于 SQLite 的轻量级和嵌入式特性,它被广泛用于移动应用程序中,包括 iOS 和 Android 平台上的应用。它可以用于存储用户数据、应用程序配置和缓存等。
  2. 嵌入式系统:SQLite 适用于嵌入式系统,如物联网设备、嵌入式设备和嵌入式软件。它可以提供一个简单的数据库解决方案,用于存储和检索设备上的数据。
  3. 桌面应用程序:SQLite 也可以用于桌面应用程序,特别是那些需要本地存储数据的应用。它可以用于创建轻量级的数据库驱动的应用程序,如个人信息管理工具、笔记应用等。
  4. 测试和原型开发:由于 SQLite 的易用性和快速部署特性,它常被用于测试和原型开发阶段。开发人员可以快速创建数据库结构,并使用 SQLite 进行数据存储和检索。

注意事项:

  1. 并发性限制:SQLite 是一个单用户数据库,不支持多个进程同时对同一个数据库文件进行写操作。并发读取是支持的,但写操作需要进行锁定。因此,在高并发写入场景下,需要谨慎使用SQLite。
  2. 数据库大小限制:SQLite 对数据库文件的大小有一定限制,通常是几 TB。如果需要处理大型数据集或需要高性能的数据库操作,SQLite 可能不是最佳选择。
  3. 数据类型限制:SQLite 支持多种数据类型,但没有严格的数据类型检查。这意味着在存储数据时需要注意数据类型的一致性,以避免数据损坏或错误。

类似竞品: SQLite 在嵌入式数据库领域有一些类似的竞品,其中一些包括:

  1. MySQL: MySQL 是一个功能强大的关系型数据库管理系统,与 SQLite 相比,它是一个独立的服务器进程,适用于需要处理大量数据和高并发写入的场景。
  2. PostgreSQL: PostgreSQL 是另一个功能丰富的关系型数据库管理系统,它提供更高级的功能和更复杂的查询支持,适用于大型应用程序和复杂数据模型。
  3. MongoDB:MongoDB 是一个面向文档的 NoSQL 数据库,适用于处理非结构化数据和需要高度可扩展性的应用程序。

3.2.2. SQLite 具有的一些特性

  1. 轻量级:SQLite 是一个轻量级的数据库引擎,它的核心库非常小巧,占用资源少,适合在资源有限的环境中使用。
  2. 嵌入式:SQLite 可以直接嵌入到应用程序中,无需独立的服务器进程。它使用单个文件存储整个数据库,便于部署和管理。
  3. 无配置:SQLite 不需要复杂的配置过程,可以在没有任何额外设置的情况下立即使用。只需打开数据库文件,即可开始进行数据存储和检索。
  4. 事务支持:SQLite 支持 ACID(原子性、一致性、隔离性和持久性)事务,确保数据的完整性和一致性。它可以通过事务回滚来撤销未提交的更改。
  5. SQL 兼容:SQLite 支持标准的 SQL 查询语言,包括常见的 SQL 语句、数据类型和操作符。它还支持复杂的查询、连接和子查询等高级 SQL 功能。
  6. 跨平台:SQLite 可以在各种操作系统上运行,包括 Windows、macOS、Linux 和嵌入式系统等。它提供了多种编程语言的接口,如 C、C++、Java、Python 等。
  7. 零配置备份和恢复:SQLite 提供了简单的备份和恢复机制,只需复制数据库文件即可完成备份。这使得数据的备份和迁移变得非常方便。
  8. 小内存占用:SQLite 在内存使用方面非常高效,可以在内存受限的环境中运行。它可以通过调整缓存大小和使用内存模式来进一步优化内存占用。
  9. 支持扩展:SQLite 提供了扩展机制,允许开发人员编写自定义的函数、聚合函数和虚拟表等,以满足特定的需求。 这些特性使得 SQLite 成为一个简单、灵活且易于使用的数据库解决方案,适用于许多不同的应用场景。

3.2.3. SQLite 的安装

Mac 下如何检测是否安装了 SQLite?以及如何安装 SQLite?

1
2
3
4
5
# 检测是否已安装SQLite:
sqlite3 --version

# 安装SQLite
brew install sqlite

通用安装流程:

  1. 下载 SQLite:首先,你需要从 SQLite 官方网站(https://www.sqlite.org/download.html)下载适合你操作系统的SQLite预编译二进制文件。选择与你的操作系统和架构相匹配的版本进行下载。
  2. 安装 SQLite:解压下载的 SQLite 二进制文件到你选择的目录。在 Windows 上,你可以将 SQLite 文件放在一个易于访问的位置,如 C:\sqlite。在 Linux 和 macOS 上,你可以将 SQLite 文件放在/usr/local/或/opt/等目录下。
  3. 设置环境变量(可选):为了方便在命令行中直接使用 SQLite,你可以将 SQLite 的安装目录添加到系统的 PATH 环境变量中。这样,你就可以在任何位置使用 sqlite3 命令来启动 SQLite 控制台。
  4. 启动 SQLite 控制台:打开终端或命令提示符,导航到 SQLite 的安装目录(如果没有设置环境变量),然后运行 sqlite3 命令。这将启动 SQLite 控制台,并显示 SQLite 的版本信息。
  5. 创建或打开数据库:在 SQLite 控制台中,你可以使用 SQLite 的命令来创建新的数据库文件或打开现有的数据库文件。例如,要,可以运行以下命令:

3.2.4. SQLite 的 SQL 基本操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 创建一个名为mydatabase.db的新数据库文件
# 如果数据库文件已经存在,可以使用相同的命令打开它。
sqlite> .open mydatabase.db

# .help帮助
sqlite> .help

# 执行SQL命令
sqlite> CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER);
sqlite> INSERT INTO users (name, age) VALUES ('John', 25);
sqlite> SELECT * FROM users;
sqlite> UPDATE table_name SET column1 = value1, column2 = value2, ... WHERE condition;
sqlite> DELETE FROM table_name WHERE condition;
sqlite> ALTER TABLE table_name ADD COLUMN new_column datatype constraint;
sqlite> DROP TABLE table_name;
sqlite> CREATE INDEX index_name ON table_name (column1, column2, ...);

# SQLite支持数据类型,同时增加索引(索引底层数据结构包含B树、Hash)
sqlite> CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT,
    age INTEGER,
    salary REAL,
    profile_image BLOB
		INDEX idx_name (name)
);

# DB包含哪些表
sqlite> .tables
sqlite> SELECT name FROM sqlite_master WHERE type='table';

# 表结构查看
sqlite> .schema already_updated_blogs
sqlite> PRAGMA table_info(already_updated_blogs);

# 清空表
DELETE FROM table_name; -- 保留了表结构不变

# 使用.quit命令退出SQLite控制台
sqlite> .quit

# 其他
sqlite> select current_timestamp; # UTC时间
sqlite> datetime('now', 'localtime') # 本地当前时间

除了使用 SQLite 控制台,你还可以在你喜欢的编程语言中使用 SQLite。SQLite 提供了多种编程语言的接口,如 C、C++、Java、Python 等。你可以使用相应语言的 SQLite 库来连接和操作 SQLite 数据库。

3.2.5. SQLite 使用遇到的一些小问题

不支持表创建后调整表的列顺序,可以创建一个新表,从旧表查询数据插入新表,删除旧表,再重复一遍上面的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 创建临时表
CREATE TABLE table_temp (
  column1 TEXT,
  column2 INTEGER,
  column3 TEXT
);
# 导入数据到临时表
INSERT INTO table_temp (column1, column2, column3) SELECT column1, column2, column3 FROM old_table;

# 删除旧表
DROP old_table

# 创建新表(按最新的命名)
CREATE TABLE old_table (
  column1 TEXT,
  column2 INTEGER,
  column3 TEXT
);
# 导入临时表数据到新表
# 删除临时表

4. 开发过程中的一些问题

4.1. OpenAI 相关相关

4.1.1. token 使用量消耗过大

因为通过 OpenAI 调用 AI 服务是按 token 收费的,所有可以考虑将一些无关紧要的字符移除,以减少 token 大小。

解决方案:这里我是将代码部分全部替换掉,仅保留文字以减少 OpenAI 的 token 使用量,因为 Blog 大部分内容仍然是文本信息,代码部分缺失不影响整个文章内容的摘要提取,目前对比看下来效果没有什么影响。

4.1.2. token 过长超过了模型的最大阈值

因为 Blog 的 md 中只有一篇有这个问题,暂时没有投入继续处理。

解决思路:可以将 token 过长的请求文本,考虑拆分成多个文本分别请求 OpenAI,得到结果后再喂给 OpenAI(有点类似 LangChain 的思想)

4.1.3. OpenAI API 接口限频问题

token 限频问题,执行过程遇到每分钟超过阈值问题?

解决思路:考虑 Token 技术+滑动窗口设计(后续有时间再考虑):

  • 即每分钟固定阈值的 Token,每次 OpenAI 调用后,扣除对应的令牌桶令牌,到下一分钟重置令牌桶中数据
  • 如果令牌桶中潜在资源过少,则阻塞下一个 AISummary 协程的并发处理,直至令牌桶中数据重置的通知

5. 最后

5.1. 与 AI 协同工作和学习

通常我们开发过程中也会遇到一些类似文章开头的问题,面对一个像解决的问题,我们有自己的初步想法,但实现过程中会存在一些知识盲区。

以往,当我遇到类似问题我基本会通过 Wikipedia、Google、StackOverflow、官方文档查阅,然后综合系统理解后给出解决方案,最后问题解决。虽然查阅资料和笔记过程会有技术的积累沉淀与技术成长,但效率还是蛮慢的。

现在,当我遇到了一个我不清楚的问题或者概念,会先通过 OpenAI 了解这个问题物的概念是什么、应用场景是什么、竞品是什么、有哪些优势和劣势等,实际就是 SWOT 分析,然后在让 OpenAI 结合对应的场景,给出可选的解决方案(就比如这次我对 SQLite 的使用 case),针对一些偏更深层次的内容,会通过 Google、Github、Medium 等平台去寻找一些最新相关的讯息,然后我会将这些信息通过 Notion 给记录下来,最终沉淀到自己的知识体系内,内化为自己的经验。

总之,我发现目前已经习惯了有 OpenAI 辅助研发、阅读、翻译、编程的场景了,无论是从知识的系统性学习,还是工作还是学习的效率都有蛮大的提升(趁手的工具很重要)! 所以面对新的知识,善用 OpenAI - What,Why,How! 自从 OpenAI 推出这大半年以来,在遇到自己盲区的问题,大约 70%以上问题我会优先考虑使用 OpenAI 来协同解决,怪不得之前 OpenAI 出来时候,Google 的搜索股价大跌了一波.

5.2. OpenAI 使用技巧

  1. 网络问题是基础设施必须要解决的,参考之前写的 cloudflare-wrap 那篇文章: https://tkstorm.com/ln/cloudflare-warp
  2. 选择配套的趁手的 AI 辅助工具很重要(包含账号注册等),参考之前写得 chatgpt-tips 那篇文章,后续也会持续更新: https://tkstorm.com/ln/chatgpt-tips
  3. Prompt 提示词,这个对你问题得到的结果准确性影响很大,可以参考之前写的 chatgpt-prompt: https://tkstorm.com/ln/chatgpt-prompt
  4. OpenAI 接口,可以尝试用 OpenAI Restful 接口进行一些基本的辅助开发,后续可以进一步结合langchain之类的进行深层次应用(这部分还在探索阶段)
  5. 基于 OpenAI 开发一些应用,并定期输出到 Blog 进行总结(这部还在进行中)

5.3. OpenAI 费用问题

目前主要是通过 OpenAI 进行接口请求,配套的ChatGPT-Next Web的Mac客户端(很好用),个人日常使用平均每月在 5 美刀左右(通过 Nobepay 充值),比 20 美刀每月的 ChatGPT 体验还有流畅度要好很多。

5.4. 个人 AI 日常工具截图

这里简单罗列了个人常用的几款 AI 高频辅助工具,有兴趣的自己可以尝试下~

5.4.1. ChatGPT-Next Web: 一个 OpenAI 的 Mac 客户端(强烈推荐)

5.4.2. Popclip+Bob+OpenAI: 翻译+Wikipedia 的组合

5.4.3. glarity summary : 一个 Chrome 插件,支持汇总文章摘要、重要信息