AI 服务架构与自然语言调度
分析 go-ai-scheduler 中 AI 服务的架构位置、自然语言到任务定义的转换流程,以及 AI 仅建议不决策的设计哲学。
问题背景
在调度系统中引入 AI,最常见的需求有两个:
- 自然语言创建任务:用户说"每 5 分钟备份一次数据库",系统自动生成对应的 cron 任务
- 智能运维: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 表达式和具体命令,会回复:
"我可以帮你创建这个任务,但需要补充几个信息:
- 备份频率是多少?(如每 5 分钟、每天凌晨 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 状态 | ❌ | |
| 直接操作数据库 | ❌ |
为什么这样设计?
- 可预测性:调度系统的行为应该是确定性的,不应该因为 LLM 的随机性而变化
- 可审计:所有任务变更都通过 API 记录,便于追溯
- 安全性:即使 LLM 被 prompt injection 攻击,也无法绕过 API 的权限校验
- 降级能力: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_health 和 get_worker_load_history 工具获取数据,由 LLM 分析后生成自然语言报告。
总结
go-ai-scheduler 的 AI 服务是一个能力受限但边界清晰的 copilot:
- 通过 Function Calling 限制 AI 的能力范围
- 通过代理模式隔离安全认证
- 通过"建议不决策"原则保证核心链路的确定性
- 通过主备模型和降级链路保证可用性
这种设计让 AI 成为了调度系统的"智能仪表盘"和"运维助手",而不是"黑盒决策者"。
下一篇会分析系统的可观测性建设,包括 Prometheus 指标、结构化日志和 Grafana 大盘的设计。