feat: 添加本地缓存功能,减少API调用
- 实现SQLite缓存模块,支持高效查询和存储 - 添加缓存键生成策略(基于原文+语言对的SHA256哈希) - 集成缓存到Translator类,先查缓存再调用API - 添加缓存管理命令:cache clear, cache stats, cache cleanup - 实现组合缓存清理策略(数量限制+时间过期) - 添加完整的单元测试 - 更新配置文件模板,添加缓存配置 - 更新文档和版本记录 版本: v0.5.1
This commit is contained in:
254
memory.md
254
memory.md
@@ -253,4 +253,256 @@ func main() {
|
||||
**注意事项**:
|
||||
- 使用 `gopkg.in/yaml.v3` 库
|
||||
- 注意缩进和格式
|
||||
- 提供配置验证
|
||||
- 提供配置验证
|
||||
|
||||
---
|
||||
|
||||
## 管道功能实现经验
|
||||
|
||||
### 管道输入检测
|
||||
**问题**: 如何检测是否有管道输入?
|
||||
**解决方案**:
|
||||
1. 使用 `os.Stdin.Stat()` 获取标准输入的状态
|
||||
2. 检查 `fileInfo.Mode() & os.ModeCharDevice` 是否为0
|
||||
3. 如果为0,表示是管道或文件重定向
|
||||
|
||||
**代码示例**:
|
||||
```go
|
||||
func isPipeInput() bool {
|
||||
fileInfo, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (fileInfo.Mode()&os.ModeCharDevice) == 0
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 在管道模式下,即使没有命令行参数也应继续执行
|
||||
- 管道输入可能为空,需要处理空输入情况
|
||||
- 错误信息应输出到stderr,避免污染管道输出
|
||||
|
||||
### 管道输入读取
|
||||
**问题**: 如何从标准输入读取所有内容?
|
||||
**解决方案**:
|
||||
1. 使用 `bufio.Scanner` 逐行读取
|
||||
2. 使用 `strings.Join()` 合并多行
|
||||
3. 处理读取错误
|
||||
|
||||
**代码示例**:
|
||||
```go
|
||||
func readFromStdin() (string, error) {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
var lines []string
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("读取标准输入失败: %w", err)
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 静默模式设计
|
||||
**问题**: 管道使用时,统计信息会污染输出
|
||||
**解决方案**:
|
||||
1. 添加 `--quiet` 和 `-q` 参数
|
||||
2. 将统计信息输出到stderr
|
||||
3. 静默模式下只输出翻译结果
|
||||
|
||||
**设计原则**:
|
||||
- 翻译结果始终输出到stdout
|
||||
- 统计信息输出到stderr
|
||||
- 静默模式下抑制统计信息输出
|
||||
- 错误信息始终输出到stderr
|
||||
|
||||
### 正则表达式转义
|
||||
**问题**: 特殊字符在正则表达式中需要转义
|
||||
**解决方案**:
|
||||
1. 使用 `regexp.QuoteMeta()` 转义特殊字符
|
||||
2. 避免手动拼接正则表达式
|
||||
|
||||
**代码示例**:
|
||||
```go
|
||||
// 错误的方式
|
||||
pattern := regexp.MustCompile(symbol + `{21,}`) // 如果symbol是*会出错
|
||||
|
||||
// 正确的方式
|
||||
pattern := regexp.MustCompile(regexp.QuoteMeta(symbol) + `{21,}`)
|
||||
```
|
||||
|
||||
**特殊字符列表**:
|
||||
- `*`, `+`, `?`, `.`, `^`, `$`, `|`, `(`, `)`, `[`, `]`, `{`, `}`
|
||||
|
||||
### 输出重定向
|
||||
**问题**: 如何确保管道输出不被其他信息污染?
|
||||
**解决方案**:
|
||||
1. 翻译结果使用 `fmt.Println()` 输出到stdout
|
||||
2. 统计信息使用 `fmt.Fprintf(os.Stderr, ...)` 输出到stderr
|
||||
3. 错误信息也输出到stderr
|
||||
|
||||
**最佳实践**:
|
||||
- 始终区分stdout和stderr的使用场景
|
||||
- 为管道功能提供静默模式选项
|
||||
- 确保错误信息不影响管道输出
|
||||
|
||||
---
|
||||
|
||||
## 本地缓存实现经验
|
||||
|
||||
### SQLite数据库设计
|
||||
**问题**: 如何设计缓存表结构以支持高效的查询和存储?
|
||||
**解决方案**:
|
||||
1. 使用缓存键(cache_key)作为唯一索引
|
||||
2. 包含完整字段:原文、译文、语言对、模型、Prompt、用量统计
|
||||
3. 添加created_at和last_used_at时间戳字段
|
||||
4. 创建适当的索引提高查询性能
|
||||
|
||||
**表结构**:
|
||||
```sql
|
||||
CREATE TABLE translation_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cache_key TEXT NOT NULL UNIQUE,
|
||||
original_text TEXT NOT NULL,
|
||||
translated_text TEXT NOT NULL,
|
||||
from_lang TEXT NOT NULL,
|
||||
to_lang TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
prompt_name TEXT,
|
||||
prompt_content TEXT,
|
||||
prompt_tokens INTEGER DEFAULT 0,
|
||||
completion_tokens INTEGER DEFAULT 0,
|
||||
total_tokens INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**索引设计**:
|
||||
```sql
|
||||
CREATE INDEX idx_cache_key ON translation_cache(cache_key);
|
||||
CREATE INDEX idx_original_text ON translation_cache(original_text);
|
||||
CREATE INDEX idx_created_at ON translation_cache(created_at);
|
||||
CREATE INDEX idx_last_used_at ON translation_cache(last_used_at);
|
||||
```
|
||||
|
||||
### 缓存键生成策略
|
||||
**问题**: 如何生成唯一的缓存键,确保相同内容返回相同结果?
|
||||
**解决方案**:
|
||||
1. 使用SHA256哈希算法
|
||||
2. 哈希输入:原文+语言对(from_lang+to_lang)
|
||||
3. 规范化输入:移除多余空白字符,统一语言代码格式
|
||||
|
||||
**代码示例**:
|
||||
```go
|
||||
func GenerateCacheKey(originalText, fromLang, toLang string) string {
|
||||
// 规范化语言代码
|
||||
fromLang = normalizeLanguageCode(fromLang)
|
||||
toLang = normalizeLanguageCode(toLang)
|
||||
|
||||
// 规范化原文
|
||||
normalizedText := normalizeText(originalText)
|
||||
|
||||
// 生成缓存键
|
||||
data := fmt.Sprintf("%s|%s|%s", normalizedText, fromLang, toLang)
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
```
|
||||
|
||||
### SQLite NULL值处理
|
||||
**问题**: 当缓存表为空时,MIN(created_at)返回NULL,导致时间转换错误
|
||||
**解决方案**:
|
||||
1. 使用sql.NullString和sql.NullFloat64类型
|
||||
2. 检查Valid字段判断是否为NULL
|
||||
3. 只在有记录时才查询时间范围
|
||||
|
||||
**代码示例**:
|
||||
```go
|
||||
var oldestStr, newestStr sql.NullString
|
||||
var avgTokens sql.NullFloat64
|
||||
err = c.db.QueryRowContext(ctx, `
|
||||
SELECT MIN(created_at), MAX(created_at), AVG(total_tokens)
|
||||
FROM translation_cache
|
||||
`).Scan(&oldestStr, &newestStr, &avgTokens)
|
||||
|
||||
if oldestStr.Valid {
|
||||
stats.OldestRecord, _ = time.Parse("2006-01-02 15:04:05", oldestStr.String)
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存集成策略
|
||||
**问题**: 如何将缓存功能集成到现有的翻译流程中?
|
||||
**解决方案**:
|
||||
1. 在Translator结构中添加缓存字段
|
||||
2. 修改NewTranslator函数,初始化缓存实例
|
||||
3. 在Translate方法中实现"先查缓存再调用API"策略
|
||||
4. 翻译成功后异步保存到缓存
|
||||
|
||||
**集成流程**:
|
||||
```
|
||||
翻译请求 → 生成缓存键 → 查询缓存 → 缓存命中? → 直接返回
|
||||
↓ 否
|
||||
调用API翻译 → 保存到缓存 → 返回结果
|
||||
```
|
||||
|
||||
### 异步缓存保存
|
||||
**问题**: 如何避免缓存保存阻塞翻译结果返回?
|
||||
**解决方案**:
|
||||
1. 使用goroutine异步保存缓存
|
||||
2. 使用context.Background()创建新的上下文
|
||||
3. 错误日志记录到标准错误输出
|
||||
|
||||
**代码示例**:
|
||||
```go
|
||||
// 异步保存缓存,不阻塞翻译结果返回
|
||||
go t.cache.Set(context.Background(), cacheEntry)
|
||||
```
|
||||
|
||||
### 组合清理策略
|
||||
**问题**: 如何平衡缓存大小和缓存时效性?
|
||||
**解决方案**:
|
||||
1. 数量限制:限制最大记录数
|
||||
2. 时间过期:设置缓存过期时间
|
||||
3. 组合策略:同时应用两种限制
|
||||
|
||||
**清理逻辑**:
|
||||
1. 清理过期记录:`DELETE FROM cache WHERE last_used_at < ?`
|
||||
2. 清理超出数量限制:保留最近使用的N条记录
|
||||
3. 定时清理:每小时自动清理一次
|
||||
|
||||
### 性能优化
|
||||
**SQLite性能优化**:
|
||||
1. 使用WAL模式(Write-Ahead Logging)
|
||||
2. 设置适当的连接池参数
|
||||
3. 使用索引提高查询性能
|
||||
4. 批量操作使用事务
|
||||
|
||||
**Go SQLite配置**:
|
||||
```go
|
||||
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_synchronous=NORMAL")
|
||||
db.SetMaxOpenConns(1) // SQLite只支持单个写入连接
|
||||
db.SetMaxIdleConns(1)
|
||||
```
|
||||
|
||||
### 错误处理经验
|
||||
**常见错误及解决方案**:
|
||||
1. **NULL值转换错误**: 使用sql.NullString类型
|
||||
2. **时间解析错误**: 使用正确的时间格式字符串
|
||||
3. **数据库锁定错误**: 设置适当的连接池参数
|
||||
4. **权限错误**: 确保目录存在且有写入权限
|
||||
|
||||
### 测试策略
|
||||
**单元测试要点**:
|
||||
1. 测试缓存键生成的一致性
|
||||
2. 测试缓存命中和未命中情况
|
||||
3. 测试清理策略的正确性
|
||||
4. 测试NULL值处理
|
||||
5. 测试并发访问安全性
|
||||
|
||||
**集成测试要点**:
|
||||
1. 测试缓存与Translator的集成
|
||||
2. 测试配置文件的正确加载
|
||||
3. 测试缓存命令的正确执行
|
||||
4. 测试错误情况的正确处理
|
||||
Reference in New Issue
Block a user