基于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.Bufferbufio 缓冲库很类似,后者支持将一个 io.Writerio.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 插件,支持汇总文章摘要、重要信息