Files
yoyo/memory.md
z.to b71f76c8b3 feat: 添加本地缓存功能,减少API调用
- 实现SQLite缓存模块,支持高效查询和存储
- 添加缓存键生成策略(基于原文+语言对的SHA256哈希)
- 集成缓存到Translator类,先查缓存再调用API
- 添加缓存管理命令:cache clear, cache stats, cache cleanup
- 实现组合缓存清理策略(数量限制+时间过期)
- 添加完整的单元测试
- 更新配置文件模板,添加缓存配置
- 更新文档和版本记录

版本: v0.5.1
2026-03-29 21:10:28 +08:00

14 KiB
Raw Blame History

记忆纠正 (memory.md)

本文档记录开发过程中的重要定义、踩过的坑和经验总结用于AI内部知识纠正。

使用说明

  • 定期更新,特别是遇到问题后
  • 按类别组织,便于查找
  • 包含具体示例和解决方案

技术决策记录

Go语言选择

决策: 使用Go语言而非Node.js或Deno
原因:

  1. Go编译为单一二进制文件部署方便
  2. 性能优秀适合CLI工具
  3. 强大的标准库支持
  4. 用户愿意学习Go

影响: 项目结构、依赖管理、开发工具链


面向对象设计

决策: 在Go中实现面向对象设计模式
模式应用:

  1. 工厂模式: ProviderFactory创建厂商实例
  2. 策略模式: Provider接口定义不同厂商策略
  3. 依赖注入: Translator依赖Provider接口

注意事项:

  • Go中没有类使用结构体
  • 继承通过组合实现
  • 多态通过接口实现

术语定义

厂商 (Provider)

指提供大模型API的服务商如硅基流动、火山引擎等。每个厂商实现统一的Provider接口。

配置 (Config)

全局配置对象包含API密钥、模型选择、超时设置等。使用YAML格式。

核心翻译器 (Translator)

负责协调配置、厂商和Prompt管理执行翻译任务的核心类。

踩过的坑

环境变量加载问题

问题: 配置文件中的环境变量(如${API_KEY})没有正确解析
原因: 没有实现环境变量替换功能
解决方案:

  1. 使用os.Getenv读取环境变量
  2. 在配置加载时进行字符串替换
  3. 添加环境变量验证

预防措施:

  • 在配置加载后验证所有必需的环境变量
  • 提供清晰的错误信息

厂商API差异

问题: 不同厂商的API格式差异较大
原因: 每个厂商有自己的请求/响应格式
解决方案:

  1. 定义统一的TranslateRequestTranslateResponse
  2. 在每个厂商实现中进行格式转换
  3. 使用适配器模式

经验:

  • 优先设计统一接口
  • 将厂商特定逻辑封装在实现内部
  • 提供原始响应用于调试

版本号管理混乱

问题: 版本号递增规则不明确
原因: 没有明确的版本管理规范
解决方案:

  1. 采用语义化版本:主版本.次版本.修订版本
  2. 第三位限制为00-99
  3. 建立更新流程

规范:

  • 小修复:修订版本+1
  • 新功能:次版本+1修订版本重置为00
  • 重大变更:主版本+1次版本和修订版本重置

环境变量加载问题

问题: 配置文件中的环境变量没有正确加载
原因: Go程序不会自动加载.env文件需要使用第三方库
解决方案:

  1. 使用github.com/joho/godotenv
  2. 在程序启动时调用godotenv.Load()
  3. 将.env文件添加到.gitignore

代码示例:

import "github.com/joho/godotenv"

func main() {
    _ = godotenv.Load() // 加载.env文件
    // 然后加载配置文件
}

注意事项:

  • 不要提交真实的.env文件到版本控制
  • 提供.env.example模板
  • 在文档中说明环境变量配置方法

配置最佳实践

安全配置

  • API密钥使用环境变量
  • 不提交敏感信息到版本控制
  • 提供.env.example模板

配置验证

  • 启动时验证所有必需配置
  • 提供有意义的错误信息
  • 支持配置热重载(未来)

默认值策略

  • 为非必需配置提供合理的默认值
  • 默认值应在config.setDefaults()中设置
  • 记录默认值的作用

开发工作流

日常开发流程

  1. dev分支创建功能分支
  2. 开发并测试功能
  3. 更新相关文档taolun.md、changelog.md
  4. 提交到功能分支
  5. 合并到dev分支
  6. 测试通过后合并到main

版本发布流程

  1. 确保dev分支稳定
  2. 更新版本号
  3. 更新changelog.md
  4. 创建版本标签
  5. 合并到main分支

文档维护

  • 每次重要讨论后更新taolun.md
  • 每个版本更新changelog.md
  • 遇到问题后更新memory.md

文档管理规范

why.md (项目初衷文档)

用途: 记录项目初衷、愿景、目标和个人笔记
编辑权限: 只能由项目所有者(用户)编辑
位置: 项目根目录
内容建议:

  • 项目愿景
  • 核心问题
  • 目标用户
  • 期望功能
  • 个人笔记

注意事项:

  • AI不应修改此文件
  • 文件内容反映创始人的个人想法
  • 可以自由格式,不强制结构

文档协作规范

taolun.md: AI与用户共同维护的讨论记录
changelog.md: AI与用户共同维护的版本记录
memory.md: AI主导的知识纠正和经验总结
why.md: 用户专属的项目初衷文档

编辑流程:

  1. 用户编辑why.md记录初衷
  2. AI编辑taolun.md记录讨论
  3. AI更新changelog.md记录版本
  4. AI更新memory.md记录经验

语言代码处理经验

语言代码标准化

问题: 需要支持多种语言代码格式,但内部应使用标准格式
解决方案:

  1. 使用BCP 47语言标签作为标准格式zh-CNen-US
  2. 实现智能解析函数 ParseLanguageCode()
  3. 支持别名映射(如 cnzh-CNenen-US

最佳实践:

  • 语言代码小写,地区代码大写(如 zh-CN,不是 zh-cn
  • 提供语言名称映射用于显示(如 zh-CN → "中文(简体)"
  • 支持模糊匹配和建议功能

交互式配置经验

问题: 命令行工具需要友好的配置界面
解决方案:

  1. 使用 github.com/AlecAivazis/survey/v2
  2. 实现分步配置流程
  3. 提供默认值和确认选项

注意事项:

  • 交互式库需要终端支持
  • 提供非交互式模式(如配置文件模板)
  • 错误处理要友好,避免程序崩溃

命令行参数解析经验

问题: Go标准库 flag 包功能有限,需要支持子命令
解决方案:

  1. 使用 flag 包解析选项参数
  2. 手动处理子命令(如 onboard
  3. 提供清晰的帮助信息

命名冲突处理:

  • 避免变量名与包名冲突(如 onboard 变量与 onboard 包)
  • 使用后缀区分(如 onboardFlag

配置文件管理经验

开发阶段配置策略

决策: 开发阶段使用 .env + configs/config.yaml
原因:

  1. 简化开发环境配置
  2. 符合12-factor应用原则
  3. 避免过早优化

实施:

  • .env 文件存储API密钥等敏感信息
  • configs/config.yaml 存储复杂配置结构
  • 使用环境变量替换 ${VAR}

配置文件格式选择

决策: 使用YAML格式
原因:

  1. 人类可读性好
  2. 支持复杂数据结构
  3. Go生态支持良好

注意事项:

  • 使用 gopkg.in/yaml.v3
  • 注意缩进和格式
  • 提供配置验证

管道功能实现经验

管道输入检测

问题: 如何检测是否有管道输入?
解决方案:

  1. 使用 os.Stdin.Stat() 获取标准输入的状态
  2. 检查 fileInfo.Mode() & os.ModeCharDevice 是否为0
  3. 如果为0表示是管道或文件重定向

代码示例:

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. 处理读取错误

代码示例:

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. 避免手动拼接正则表达式

代码示例:

// 错误的方式
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. 创建适当的索引提高查询性能

表结构:

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
);

索引设计:

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. 规范化输入:移除多余空白字符,统一语言代码格式

代码示例:

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. 只在有记录时才查询时间范围

代码示例:

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 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配置:

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. 测试错误情况的正确处理