AI 服务架构与自然语言调度

分析 go-ai-scheduler 中 AI 服务的架构位置、自然语言到任务定义的转换流程,以及 AI 仅建议不决策的设计哲学。

问题背景

在调度系统中引入 AI,最常见的需求有两个:

  1. 自然语言创建任务:用户说"每 5 分钟备份一次数据库",系统自动生成对应的 cron 任务
  2. 智能运维:AI 分析失败日志,给出修复建议,甚至自动重试

但这两个需求都面临同一个挑战:如何让 AI 既足够聪明,又不会越权操作?

go-ai-scheduler 的解决方案是**"AI 仅建议不决策"**——AI 可以查询状态、分析原因、创建任务定义,但任务的实际触发和执行仍由调度器控制。

AI 服务的架构位置

AI 服务(ai-service)是四服务架构中最独立的一个:

┌─────────────┐     HTTP      ┌─────────────┐
│   api       │ ◄───────────► │ ai-service  │
│  (:8082)    │   (proxy)     │  (:8083)    │
└──────┬──────┘               └─────────────┘
       │
       ▼
┌─────────────┐
│ scheduler   │◄──── AI 不直接访问
│  (:8081)    │
└─────────────┘

api 服务作为 AI 的代理层,负责:

  • 鉴权(JWT/RBAC)
  • 限流(每 IP 每小时 5 次 AI 请求)
  • 请求转发到 ai-service
  • 响应格式化

这种"代理模式"让 AI 服务不需要关心安全认证,专注于推理逻辑。

自然语言到任务定义

AI 服务的核心能力之一是将自然语言转换为结构化的任务定义。这个过程通过 create_task 工具实现。

用户输入示例

"创建一个每 5 分钟执行一次的 Shell 任务,运行 /opt/scripts/backup.sh"

Agent 的处理流程

用户输入
  → LLM 解析意图
  → 提取参数:name, type=shell, cron_expr="*/5 * * * *", payload="/opt/scripts/backup.sh"
  → 调用 create_task 工具
  → 工具校验参数(cron 合法性、必填项)
  → 写入数据库
  → 返回创建结果

参数补全

如果用户输入信息不全,LLM 会主动询问:

"创建一个数据库备份任务"

LLM 识别到缺少 cron 表达式和具体命令,会回复:

"我可以帮你创建这个任务,但需要补充几个信息:

  1. 备份频率是多少?(如每 5 分钟、每天凌晨 2 点)
  2. 备份脚本的路径是什么?"

这种交互模式由系统提示词中的约束驱动:

const SystemPrompt = `
当用户要创建一个任务但信息不全时,主动询问缺失的信息
(名称、类型、cron 表达式、执行内容)。
`

LLM 适配器:多模型支持

AI 服务通过 LLMAdapter 封装了不同 LLM 提供商的接口:

// internal/ai/adapter/adapter.go
type LLMAdapter struct {
    endpoint        string
    apiKey          string
    model           string
    fallback        *LLMAdapter
    totalPrompt     atomic.Int64
    totalCompletion atomic.Int64
}

主备模型

Adapter 支持配置 fallback 模型:

func (a *LLMAdapter) SetFallback(fb *LLMAdapter) {
    if a != nil && fb != nil && fb.Enabled() {
        a.fallback = fb
    }
}

当主模型不可用时,自动切换到备用模型。这在生产环境中很重要——即使主模型服务宕机,AI 功能仍能降级运行。

Token 统计

Adapter 内部维护了 Token 使用计数:

func (a *LLMAdapter) TokenUsage() (prompt, completion int64) {
    return a.totalPrompt.Load(), a.totalCompletion.Load()
}

这些数据可以通过 /metrics 端点暴露,接入成本监控系统。

流式输出与 WebSocket

AI 服务支持两种实时输出模式:

SSE(Server-Sent Events)

适用于前端直接消费的场景:

前端 ──HTTP/SSE──► ai-service ──HTTP/SSE──► LLM API
        ▲                                    │
        └──────── 实时推送文本 ◄─────────────┘

WebSocket

适用于需要双向通信的场景(如用户中断生成):

前端 ◄──WebSocket──► ai-service ──HTTP/SSE──► LLM API

WebSocket 层负责维护连接状态,将用户的消息转发给 Agent,同时将 Agent 的 SSE 流转义为 WebSocket 消息。

AI 仅建议不决策

这是 go-ai-scheduler 最重要的设计约束。AI 服务在架构上被限制在"建议层":

能力AI 可以AI 不可以
查询任务状态
创建任务定义
分析失败原因
触发任务执行
修改 Worker 状态
直接操作数据库

为什么这样设计?

  1. 可预测性:调度系统的行为应该是确定性的,不应该因为 LLM 的随机性而变化
  2. 可审计:所有任务变更都通过 API 记录,便于追溯
  3. 安全性:即使 LLM 被 prompt injection 攻击,也无法绕过 API 的权限校验
  4. 降级能力:AI 服务宕机不影响核心调度功能

<Callout type="warning" title="架构层面的硬性隔离"

AI 服务没有 scheduler 的数据库连接,也没有 scheduler 的 gRPC 客户端。它只能通过 HTTP API 与 api 服务通信,而 api 服务不会暴露触发执行的接口给 AI。

AI 辅助的调度建议

除了对话式 Agent,AI 服务还提供调度建议功能。它分析历史执行数据,给出优化建议:

  • ** throttle 建议**:某 Worker 负载持续高于 90%,建议扩容或降低任务频率
  • migrate 建议:某任务在特定 Worker 上失败率高于平均水平,建议调整标签路由
  • scale 建议:整体 pending 数持续增长,建议增加 Worker 节点

这些建议通过 get_system_healthget_worker_load_history 工具获取数据,由 LLM 分析后生成自然语言报告。

总结

go-ai-scheduler 的 AI 服务是一个能力受限但边界清晰的 copilot

  • 通过 Function Calling 限制 AI 的能力范围
  • 通过代理模式隔离安全认证
  • 通过"建议不决策"原则保证核心链路的确定性
  • 通过主备模型和降级链路保证可用性

这种设计让 AI 成为了调度系统的"智能仪表盘"和"运维助手",而不是"黑盒决策者"。

下一篇会分析系统的可观测性建设,包括 Prometheus 指标、结构化日志和 Grafana 大盘的设计。