跳转至

设计决策

本文档解释了 clinvoker 开发过程中的关键架构决策及其背后的原理。理解这些决策有助于开发者有效贡献,并帮助用户理解系统的行为。

为什么选择 Go

决策

clinvoker 使用 Go(Golang)实现。

原理

  1. 单二进制部署:Go 编译为单个静态二进制文件,没有运行时依赖,使分发变得简单。

  2. 出色的并发性:Go 的 goroutine 和 channel 提供了轻量级并发原语,非常适合处理多个后端和请求。

  3. 标准库:丰富的标准库包括 HTTP 服务器、JSON 处理和子进程管理,无需外部依赖。

  4. 跨平台:原生支持 Windows、macOS 和 Linux,平台特定代码最少。

  5. 快速编译:快速的构建时间提高了开发者生产力。

考虑的替代方案

语言 优点 缺点
Python 生态系统、AI 库 部署复杂性、GIL 限制
Rust 性能、安全性 学习曲线陡峭、编译时间长
Node.js JavaScript 熟悉度 运行时依赖、回调复杂性
Java 成熟的生态系统 需要 JVM、冗长

为什么选择 Cobra 作为 CLI 框架

决策

Cobra 用作 CLI 框架。

原理

  1. 行业标准:被 Kubernetes、Hugo 和许多其他主要 Go 项目使用。

  2. 丰富功能:内置帮助生成、shell 补全和标志解析。

  3. 命令层次结构:对子命令的持久化和本地标志的自然支持。

  4. 文档:从代码自动生成文档。

  5. 验证:内置标志验证和错误处理。

实现模式

var rootCmd = &cobra.Command{
    Use:   "clinvk",
    Short: "Unified AI CLI wrapper",
    Long:  `A unified interface for Claude Code, Codex CLI, and Gemini CLI.`,
    RunE:  runRoot,
}

func init() {
    rootCmd.PersistentFlags().String("backend", "", "AI backend to use")
    rootCmd.PersistentFlags().String("model", "", "Model to use")
    // ...
}

为什么选择 Chi 作为 HTTP 路由器

决策

Chi 用作 HTTP 路由器和中间件框架。

原理

  1. 轻量级:最小开销,惯用 Go 设计。

  2. 中间件链:使用 Use() 模式的优雅中间件组合。

  3. 上下文感知:基于 context.Context 的请求范围值。

  4. URL 参数:干净的 URL 参数提取。

  5. 兼容性:与标准 http.Handler 无缝协作。

中间件栈

router := chi.NewRouter()
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
router.Use(middleware.Recoverer)
router.Use(middleware.Logger)
router.Use(middleware.Timeout(60 * time.Second))

为什么选择 Huma 作为 OpenAPI 工具

决策

Huma 用于 OpenAPI 生成和请求/响应验证。

原理

  1. 代码优先:从 Go 代码生成 OpenAPI 规范,而不是相反。

  2. 类型安全:请求/响应类型在编译时验证。

  3. 自动文档:从代码生成交互式文档。

  4. 验证:基于结构体标签的自动请求验证。

  5. 多适配器:与 Chi、Gin 和其他路由器一起工作。

示例用法

huma.Register(api, huma.Operation{
    OperationID: "create-chat-completion",
    Method:      http.MethodPost,
    Path:        "/openai/v1/chat/completions",
}, func(ctx context.Context, input *ChatRequest) (*ChatResponse, error) {
    // 处理器实现
})

为什么使用子进程执行而不是 SDK

决策

clinvoker 将 AI CLI 工具作为子进程执行,而不是直接使用它们的 SDK。

原理

  1. 零配置:CLI 工具自动处理认证、API 密钥和配置。

  2. 始终最新:SDK API 变更时无需更新 clinvoker。

  3. 功能对等:CLI 工具通常有 SDK 中没有的功能。

  4. 会话管理:利用 CLI 工具内置的会话处理。

  5. 简洁性:一个抽象层而不是多个 SDK 集成。

权衡

方面 子进程方式 SDK 方式
启动时间 略慢 更快
依赖 更少 更多库
维护 更低 更高
功能访问 完整 CLI 功能 SDK 受限
认证 CLI 处理 需要代码

SDK 兼容性方法

决策

除了原生 REST API 外,还提供 OpenAI 和 Anthropic 兼容的 API 端点。

原理

  1. 生态系统兼容性:使用 OpenAI SDK 的现有工具无需修改即可工作。

  2. 迁移路径:从云 API 轻松过渡到本地 CLI 工具。

  3. 框架支持:LangChain、LangGraph 和类似框架开箱即用。

  4. 熟悉的接口:开发者已经了解这些 API。

实现策略

flowchart TB
    subgraph 输入["客户端请求"]
        OPENAI[OpenAI 格式]
        ANTH[Anthropic 格式]
        NATIVE[原生格式]
    end

    subgraph 转换["转换层"]
        MAP[统一选项映射器]
    end

    subgraph 内部["内部处理"]
        EXEC[执行器]
        BACKEND[后端]
    end

    OPENAI --> MAP
    ANTH --> MAP
    NATIVE --> MAP
    MAP --> EXEC
    EXEC --> BACKEND

会话持久化权衡

决策

会话持久化到本地文件系统,使用 JSON 格式。

原理

  1. 简洁性:不需要外部数据库。

  2. 可移植性:易于备份、迁移和检查。

  3. 人类可读:JSON 格式允许手动检查和调试。

  4. 版本控制:如果需要,会话可以进行版本控制。

基于文件 vs 数据库存储

方面 基于文件 数据库
设置 无需设置 需要安装
复杂性 更高
查询 有限 丰富
并发 文件锁定 ACID 事务
可扩展性 单机 分布式
备份 文件复制 数据库备份

为什么不用 SQLite?

SQLite 曾被考虑但被拒绝,因为: - JSON 文件更容易检查和调试 - 没有模式迁移复杂性 - 更简单的备份和恢复 - 使用文件锁定的跨进程访问简单明了

后端抽象设计选择

决策

使用通用的 Backend 接口和统一选项映射。

原理

  1. 多态性:在核心代码中统一处理所有后端。

  2. 可扩展性:无需修改核心即可轻松添加新后端。

  3. 可测试性:用于测试的模拟后端。

  4. 一致性:无论后端如何,API 都相同。

接口设计

type Backend interface {
    Name() string
    IsAvailable() bool
    BuildCommand(prompt string, opts *Options) *exec.Cmd
    ResumeCommand(sessionID, prompt string, opts *Options) *exec.Cmd
    ParseOutput(rawOutput string) string
    ParseJSONResponse(rawOutput string) (*UnifiedResponse, error)
}

并发模型选择

决策

使用 sync.RWMutex 进行进程内并发,使用文件锁定进行跨进程同步。

原理

  1. 读密集型工作负载:大多数操作是读取(列出、获取会话)。

  2. Go 惯用:Go 中并发访问的标准模式。

  3. 跨进程安全:文件锁支持 CLI 和服务器共存。

  4. 简洁性:比基于 channel 的方法更容易理解。

并发模式

// 读操作
func (s *Store) Get(id string) (*Session, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.getLocked(id)
}

// 写操作
func (s *Store) Save(sess *Session) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    return s.saveLocked(sess)
}

配置级联设计

决策

配置遵循级联:CLI 参数 -> 环境变量 -> 配置文件 -> 默认值。

原理

  1. 可预测的覆盖:高优先级来源总是获胜。

  2. 环境友好:在容器和 CI/CD 中表现良好。

  3. 用户可控:无需更改文件即可轻松覆盖。

  4. 安全默认值:无指定时使用安全配置。

解析示例

# 配置文件:~/.clinvk/config.yaml
default_backend: claude
backends:
  codex:
    model: o3

# 环境变量
CLINVK_BACKEND=codex
CLINVK_CODEX_MODEL=o3-mini

# CLI
clinvk --backend codex "prompt"

# 结果:backend=codex (CLI), model=o3-mini (env)

HTTP 服务器设计

决策

单个二进制提供所有端点服务,支持优雅关闭。

关键特性

  1. 标准 HTTP/1.1:最大兼容性。

  2. SSE 流式传输:服务器发送事件实现实时输出。

  3. CORS 可配置:支持浏览器客户端。

  4. 健康端点:用于负载均衡器的 /health

为什么不用 gRPC?

  • HTTP 得到普遍支持
  • 浏览器兼容性很重要
  • 使用 curl 调试更简单
  • 大多数 AI SDK 使用 HTTP/REST

错误处理哲学

决策

传播带上下文的错误,优雅地失败。

原则

  1. 保留 CLI 退出码:准确传播后端错误。

  2. 结构化错误:带错误详情的 JSON 格式。

  3. 优雅降级:并行模式下返回部分结果。

  4. 详细日志:需要时提供调试信息。

错误响应格式

{
  "error": {
    "code": "backend_error",
    "message": "Claude CLI exited with code 1",
    "backend": "claude",
    "details": "rate limit exceeded"
  }
}

总结表

决策 选择 关键原因
语言 Go 单二进制、出色的并发性
CLI 框架 Cobra 行业标准、丰富功能
HTTP 路由器 Chi 轻量级、惯用 Go
OpenAPI Huma 代码优先、类型安全
执行 子进程 零配置、始终最新
API 格式 多种 框架兼容性
会话 基于文件的 JSON 简洁性、可移植性
并发 RWMutex + FileLock 读密集型、跨进程安全
配置 级联 可预测、环境友好
服务器 HTTP/SSE 通用兼容性

未来考虑

MCP 服务器支持

MCP 支持已通过 clinvk mcp 落地,支持 stdiohttp 两种 transport。

当前能力包括:

  • 将 prompt、parallel、chain、compare、session 操作暴露为 MCP tools
  • 面向本地与服务端部署场景的 transport 切换
  • 在 HTTP transport 下可选暴露 /health 端点

后续 MCP 方向:

  • 增加面向远程部署的认证模式
  • 扩展 MCP 生态互操作测试
  • 提供更细粒度的 tool 暴露策略控制

额外后端

后端抽象允许在新 AI CLI 可用时添加它们。新后端的要求:

  • CLI 支持非交互模式
  • 结构化输出(首选 JSON)
  • 会话管理(可选但首选)

相关文档