RFC: unified provider/model configuration #110

Closed
opened 2026-05-08 01:46:32 +00:00 by xiaoju · 2 comments
Owner

背景

当前 extract 的 LLM provider 是临时方案(用户级 config 单独配)。后续 core 会有更多场景需要 LLM:

场景 用途 模型要求
extract 从 LLM 输出解析结构化数据 结构化能力强
supervisor 替代 maxRounds,周期性检查 thread 健康度 便宜快速
planner plan 质量评估/自动拆分 推理能力强
summarizer 压缩上下文(未来) 平衡

需要统一的 provider/model 配置,支持按场景 override。

设计

参考 Hermes 的模式:

# ~/.uncaged/workflow/config.yaml
providers:
  anthropic:
    apiKey: ${ANTHROPIC_API_KEY}
  openrouter:
    apiKey: ${OPENROUTER_API_KEY}

models:
  default: anthropic/claude-sonnet-4
  extract: anthropic/claude-haiku
  supervisor: openrouter/gemini-2.5-flash
  planner: anthropic/claude-sonnet-4

核心规则

  1. providers 定义凭证池(provider name → apiKey)
  2. models 按场景映射(场景名 → provider/model)
  3. 缺省 fallback 到 default
  4. 环境变量 ${VAR} 语法支持
  5. 场景名由 core 预定义,用户可扩展

Phase 拆分

Phase 1: config 层

一句话:config.yaml 解析 + provider/model 解析 + fallback 逻辑

  • 验证目标:能从 config.yaml 读取并解析出指定场景的 provider + model
  • Testing issue: 待创建

Phase 2: 迁移 extract

一句话:现有 extract provider 配置迁移到新的统一配置

  • 验证目标:extract 功能用新配置正常工作,旧配置方式废弃
  • Testing issue: 待创建

Phase 3: supervisor 场景

一句话:实现 supervisor 场景,替代 maxRounds

  • 验证目标:thread 跑一段时间后 supervisor 自动检查并决定继续/停止
  • Testing issue: 待创建

完成标准

  • 所有 Phase 的 testing issue 已 close
  • 全量测试通过
  • 旧 extract provider 配置已迁移
## 背景 当前 extract 的 LLM provider 是临时方案(用户级 config 单独配)。后续 core 会有更多场景需要 LLM: | 场景 | 用途 | 模型要求 | |------|------|----------| | **extract** | 从 LLM 输出解析结构化数据 | 结构化能力强 | | **supervisor** | 替代 maxRounds,周期性检查 thread 健康度 | 便宜快速 | | **planner** | plan 质量评估/自动拆分 | 推理能力强 | | **summarizer** | 压缩上下文(未来) | 平衡 | 需要统一的 provider/model 配置,支持按场景 override。 ## 设计 参考 Hermes 的模式: ```yaml # ~/.uncaged/workflow/config.yaml providers: anthropic: apiKey: ${ANTHROPIC_API_KEY} openrouter: apiKey: ${OPENROUTER_API_KEY} models: default: anthropic/claude-sonnet-4 extract: anthropic/claude-haiku supervisor: openrouter/gemini-2.5-flash planner: anthropic/claude-sonnet-4 ``` ### 核心规则 1. **providers** 定义凭证池(provider name → apiKey) 2. **models** 按场景映射(场景名 → provider/model) 3. 缺省 fallback 到 `default` 4. 环境变量 `${VAR}` 语法支持 5. 场景名由 core 预定义,用户可扩展 ## Phase 拆分 ### Phase 1: config 层 > 一句话:config.yaml 解析 + provider/model 解析 + fallback 逻辑 - 验证目标:能从 config.yaml 读取并解析出指定场景的 provider + model - Testing issue: 待创建 ### Phase 2: 迁移 extract > 一句话:现有 extract provider 配置迁移到新的统一配置 - 验证目标:extract 功能用新配置正常工作,旧配置方式废弃 - Testing issue: 待创建 ### Phase 3: supervisor 场景 > 一句话:实现 supervisor 场景,替代 maxRounds - 验证目标:thread 跑一段时间后 supervisor 自动检查并决定继续/停止 - Testing issue: 待创建 ## 完成标准 - [ ] 所有 Phase 的 testing issue 已 close - [ ] 全量测试通过 - [ ] 旧 extract provider 配置已迁移
Author
Owner

Phase Design

Phase 1: config 层

目标:从 config.extract 单场景硬编码升级为 providers + models 多场景配置。

workflow.yaml 新格式

providers:
  anthropic:
    baseUrl: https://api.anthropic.com/v1
    apiKey: env:ANTHROPIC_API_KEY
  openrouter:
    baseUrl: https://openrouter.ai/api/v1
    apiKey: env:OPENROUTER_API_KEY

models:
  default: anthropic/claude-sonnet-4
  extract: anthropic/claude-haiku
  supervisor: openrouter/gemini-2.5-flash

# 保留
workflows: { ... }
maxDepth: 3

类型设计

// registry/types.ts — 替换 ExtractProviderConfig + WorkflowConfig

type ProviderConfig = {
  baseUrl: string;
  apiKey: string;  // resolved from env: at parse time
};

type WorkflowConfig = {
  maxDepth: number;
  providers: Record<string, ProviderConfig>;
  models: Record<string, string>;  // scene → "providerName/modelName"
};

核心 API

// 新文件:src/config/ folder (遵循 module discipline)
// config/resolve-model.ts

type ResolvedModel = {
  baseUrl: string;
  apiKey: string;
  model: string;
};

/** 按场景名解析 provider+model,fallback 到 default */
function resolveModel(config: WorkflowConfig, scene: string): Result<ResolvedModel, string>;

解析逻辑:

  1. models 里找 scene key → 得到 "providerName/modelName"
  2. 找不到就 fallback 到 models.default
  3. 用 providerName 去 providers 里取 baseUrl + apiKey
  4. 拼成 ResolvedModel

改动范围

  • registry/types.tsWorkflowConfig 新增 providers + models,删除 extract
  • registry/registry-normalize.ts — 解析新格式,resolveRegistryApiKey 扩展为支持多 provider
  • 新建 src/config/ folder(types.ts, resolve-model.ts, index.ts)
  • extract-provider.ts → 重构为调用 resolveModel(config, "extract")

不改LlmProvider 类型保留(就是 ResolvedModel 的别名),下游不感知变化。


Phase 2: 迁移 extract

目标getExtractProvider() 从直接读 config.extract 改为 resolveModel(config, "extract")

改动

  • extract-provider.ts — 用 resolveModel 替换手动拼装
  • registry-normalize.ts — 删除 normalizeExtractConfig 旧逻辑(已被新的 providers 解析替代)
  • 删除 ExtractProviderConfig 类型

迁移兼容:不做。直接 breaking change(主人的偏好),旧格式 config 解析直接报错提示升级。

验证

  1. 写一个 config.yaml 用新格式
  2. getExtractProvider() 返回正确的 baseUrl/apiKey/model
  3. extract 场景实际跑通(单测 + thread run)

Phase 3: supervisor 场景

目标:替代 maxRounds,用 LLM 周期性检查 thread 健康度。

设计

  • maxRounds 保留为硬上限(安全阀),但不再是主要停止机制
  • 每 N 轮(可配,默认 3)触发 supervisor 检查
  • supervisor 拿到当前 thread 上下文(start + 最近 steps),返回 continue | stop
  • supervisor 用 resolveModel(config, "supervisor") 取模型 — 用便宜的就行
// engine/supervisor.ts
type SupervisorDecision = "continue" | "stop";

async function runSupervisor(
  config: WorkflowConfig,
  context: ModeratorContext,
): Promise<SupervisorDecision>;

集成点engine.ts 的主循环里,每 N 轮调一次 runSupervisor

// engine.ts executeThread loop
for (const step of workflow) {
  // ... existing logic ...
  roundCount++;
  if (roundCount % supervisorInterval === 0) {
    const decision = await runSupervisor(config, ctx);
    if (decision === "stop") break;
  }
}

配置

models:
  supervisor: openrouter/gemini-2.5-flash

# 可选
supervisor:
  interval: 3       # 每 3 轮检查一次
  enabled: true     # 默认 true(如果配了 supervisor model)

改动范围

  • 新建 engine/supervisor.ts
  • engine/engine.ts — 主循环加 supervisor 钩子
  • engine/types.tsSupervisorDecision
  • registry/types.tsWorkflowConfigsupervisor 可选配置

总结依赖链:Phase 1 → Phase 2 → Phase 3,严格串行。

## Phase Design ### Phase 1: config 层 **目标**:从 `config.extract` 单场景硬编码升级为 `providers` + `models` 多场景配置。 **workflow.yaml 新格式**: ```yaml providers: anthropic: baseUrl: https://api.anthropic.com/v1 apiKey: env:ANTHROPIC_API_KEY openrouter: baseUrl: https://openrouter.ai/api/v1 apiKey: env:OPENROUTER_API_KEY models: default: anthropic/claude-sonnet-4 extract: anthropic/claude-haiku supervisor: openrouter/gemini-2.5-flash # 保留 workflows: { ... } maxDepth: 3 ``` **类型设计**: ```typescript // registry/types.ts — 替换 ExtractProviderConfig + WorkflowConfig type ProviderConfig = { baseUrl: string; apiKey: string; // resolved from env: at parse time }; type WorkflowConfig = { maxDepth: number; providers: Record<string, ProviderConfig>; models: Record<string, string>; // scene → "providerName/modelName" }; ``` **核心 API**: ```typescript // 新文件:src/config/ folder (遵循 module discipline) // config/resolve-model.ts type ResolvedModel = { baseUrl: string; apiKey: string; model: string; }; /** 按场景名解析 provider+model,fallback 到 default */ function resolveModel(config: WorkflowConfig, scene: string): Result<ResolvedModel, string>; ``` 解析逻辑: 1. 在 `models` 里找 scene key → 得到 `"providerName/modelName"` 2. 找不到就 fallback 到 `models.default` 3. 用 providerName 去 `providers` 里取 baseUrl + apiKey 4. 拼成 `ResolvedModel` **改动范围**: - `registry/types.ts` — `WorkflowConfig` 新增 `providers` + `models`,删除 `extract` - `registry/registry-normalize.ts` — 解析新格式,`resolveRegistryApiKey` 扩展为支持多 provider - 新建 `src/config/` folder(types.ts, resolve-model.ts, index.ts) - `extract-provider.ts` → 重构为调用 `resolveModel(config, "extract")` **不改**:`LlmProvider` 类型保留(就是 `ResolvedModel` 的别名),下游不感知变化。 --- ### Phase 2: 迁移 extract **目标**:`getExtractProvider()` 从直接读 `config.extract` 改为 `resolveModel(config, "extract")`。 **改动**: - `extract-provider.ts` — 用 `resolveModel` 替换手动拼装 - `registry-normalize.ts` — 删除 `normalizeExtractConfig` 旧逻辑(已被新的 providers 解析替代) - 删除 `ExtractProviderConfig` 类型 **迁移兼容**:不做。直接 breaking change(主人的偏好),旧格式 config 解析直接报错提示升级。 **验证**: 1. 写一个 `config.yaml` 用新格式 2. `getExtractProvider()` 返回正确的 baseUrl/apiKey/model 3. extract 场景实际跑通(单测 + thread run) --- ### Phase 3: supervisor 场景 **目标**:替代 `maxRounds`,用 LLM 周期性检查 thread 健康度。 **设计**: - `maxRounds` 保留为硬上限(安全阀),但不再是主要停止机制 - 每 N 轮(可配,默认 3)触发 supervisor 检查 - supervisor 拿到当前 thread 上下文(start + 最近 steps),返回 `continue | stop` - supervisor 用 `resolveModel(config, "supervisor")` 取模型 — 用便宜的就行 ```typescript // engine/supervisor.ts type SupervisorDecision = "continue" | "stop"; async function runSupervisor( config: WorkflowConfig, context: ModeratorContext, ): Promise<SupervisorDecision>; ``` **集成点**:`engine.ts` 的主循环里,每 N 轮调一次 `runSupervisor`: ```typescript // engine.ts executeThread loop for (const step of workflow) { // ... existing logic ... roundCount++; if (roundCount % supervisorInterval === 0) { const decision = await runSupervisor(config, ctx); if (decision === "stop") break; } } ``` **配置**: ```yaml models: supervisor: openrouter/gemini-2.5-flash # 可选 supervisor: interval: 3 # 每 3 轮检查一次 enabled: true # 默认 true(如果配了 supervisor model) ``` **改动范围**: - 新建 `engine/supervisor.ts` - `engine/engine.ts` — 主循环加 supervisor 钩子 - `engine/types.ts` — `SupervisorDecision` 等 - `registry/types.ts` — `WorkflowConfig` 加 `supervisor` 可选配置 --- 总结依赖链:**Phase 1 → Phase 2 → Phase 3**,严格串行。
Author
Owner

Design 更新:bundle 不再自带 provider

决定:所有 LLM provider 配置统一从 workflow.yamlproviders + models 读取,bundle 不再硬编码或参数传递 provider。

影响分析

现状(bundle 自带 provider):

// entry.ts — bundle 构建时烤进 provider
const provider = { baseUrl: "...", apiKey: requireEnv("..."), model: "qwen-plus" };
const extract = createExtract(provider);
export const run = createDevelopRun({ agent }, extract, provider);

目标(engine 注入 provider):

// entry.ts — bundle 只管业务
const agent = createHermesAgent({ ... });
export const run = createDevelopRun({ agent });

改动范围

Phase 2 扩展

  1. createWorkflow 签名变更(breaking):

    • 移除 extract: ExtractFnllmProvider: LlmProvider | null 参数
    • engine 在 executeThread 时自动 resolveModel("extract") 构建 extract
  2. WorkflowFnOptions 扩展

    • engine 注入已解析的 extract 到 options,workflow generator 内部用
    • 或者更好:engine 在 wrap generator 时自动处理 extract,bundle 完全不感知
  3. template 包简化

    • createDevelopRun(binding) — 只接 agent binding
    • createSolveIssueRun(binding) — 同上
    • 删除 extract/provider 参数
  4. bundle entry 简化

    • xiaoju-workflows/develop/entry.ts — 删除 provider 硬编码
    • xiaoju-workflows/solve-issue/entry.ts — 同上
  5. workflow-as-agent.ts

    • 已经从 config 读,改为用新的 resolveModel
  6. createExtract 变为 engine 内部

    • 不再从 @uncaged/workflow 公开导出(或标记 internal)
    • extract 构建完全由 engine 负责

新的数据流

workflow.yaml (providers + models)
  → engine.executeThread()
    → resolveModel(config, "extract") → LlmProvider
    → createExtract(provider) → ExtractFn
    → 注入到 workflow generator 内部
    → bundle 完全不感知 provider

—— 小橘 🍊(NEKO Team)

## Design 更新:bundle 不再自带 provider **决定**:所有 LLM provider 配置统一从 `workflow.yaml` 的 `providers` + `models` 读取,bundle 不再硬编码或参数传递 provider。 ### 影响分析 **现状**(bundle 自带 provider): ```typescript // entry.ts — bundle 构建时烤进 provider const provider = { baseUrl: "...", apiKey: requireEnv("..."), model: "qwen-plus" }; const extract = createExtract(provider); export const run = createDevelopRun({ agent }, extract, provider); ``` **目标**(engine 注入 provider): ```typescript // entry.ts — bundle 只管业务 const agent = createHermesAgent({ ... }); export const run = createDevelopRun({ agent }); ``` ### 改动范围 **Phase 2 扩展**: 1. **`createWorkflow` 签名变更**(breaking): - 移除 `extract: ExtractFn` 和 `llmProvider: LlmProvider | null` 参数 - engine 在 `executeThread` 时自动 `resolveModel("extract")` 构建 extract 2. **`WorkflowFnOptions` 扩展**: - engine 注入已解析的 extract 到 options,workflow generator 内部用 - 或者更好:engine 在 wrap generator 时自动处理 extract,bundle 完全不感知 3. **template 包简化**: - `createDevelopRun(binding)` — 只接 agent binding - `createSolveIssueRun(binding)` — 同上 - 删除 extract/provider 参数 4. **bundle entry 简化**: - `xiaoju-workflows/develop/entry.ts` — 删除 provider 硬编码 - `xiaoju-workflows/solve-issue/entry.ts` — 同上 5. **`workflow-as-agent.ts`**: - 已经从 config 读,改为用新的 `resolveModel` 6. **`createExtract` 变为 engine 内部**: - 不再从 `@uncaged/workflow` 公开导出(或标记 internal) - extract 构建完全由 engine 负责 ### 新的数据流 ``` workflow.yaml (providers + models) → engine.executeThread() → resolveModel(config, "extract") → LlmProvider → createExtract(provider) → ExtractFn → 注入到 workflow generator 内部 → bundle 完全不感知 provider ``` —— 小橘 🍊(NEKO Team)
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/workflow#110