Rl-Auto / README.md
Lazywords's picture
Deploy RL Auto Docker Space
c4ae742
|
Raw
History Blame Contribute Delete
20.8 kB
---
title: RL Auto
sdk: docker
app_port: 3001
pinned: false
---
# RL-Auto / Task优化
本地部署的 **RL 任务包生成 + 校验** 工具。从 echo-extension 录制包(`.zip`)出发,自动产出可直接导入插件评测的任务包:Instruction、Recommended Groups、Rubrics、Checker Catalog、DB Diff Criteria、Pass Policy、Golden Trajectory。
> **当前架构**:Docker 单端口前后端分离。`docker compose up --build` 是唯一启动方式。
> 已退役:`start.command` / Python static server / `proxy.mjs` CORS 桥。
命名对齐 echo-extension-05061347 — subtask 分组模式:
- **API 分组模式**:按 HTTP endpoint 分组,输出 `groupMode: "api"` + 完整 `apiCalls` 对象
- **MCP 分组模式**:按 MCP Tool 分组,输出 `groupMode: "mcp"` + 原始 tool 名(不会被改写为短名)
模式由后端在上传时根据 ZIP 内容自动识别(含 `mcp_calls.json` / `session.json` → mcp,否则 → api)。前端也可在 `optionsSnapshot.requestedMode` 中显式覆盖。
---
## 工程结构
```
.
├── apps/
│ ├── api/ # Node + TS 后端 (Fastify + better-sqlite3 + undici)
│ │ └── src/
│ │ ├── server.ts # 启动 + 静态托管 web-legacy + Fastify 路由
│ │ ├── routes/{health,analysis}.ts
│ │ ├── queue/ # Store 抽象 + SQLite/Postgres 仓储 + worker pool
│ │ ├── analyzers/detect.ts # ZIP → mode 自动识别
│ │ ├── ai/client.ts # 共享 AI client,多 provider 兼容
│ │ ├── judge/{runner,prompt}.ts # 6 维 Judge + pass policy
│ │ ├── config.ts # .env 解析
│ │ └── log.ts # pino,自动屏蔽 apiKey/Authorization
│ └── web-legacy/ # 当前 SPA(React + Vite,构建后由 Fastify 托管)
│ ├── src/ # 任务润色 + API/MCP 分组页面
│ ├── index.html
│ └── package.json
├── packages/
│ └── core/ # 平台无关纯逻辑(TS, ESM)
│ └── src/
│ ├── importers/{api,mcp}.ts # 解析录制 ZIP → Recording
│ ├── prompts/{shared,api,mcp}.ts # 系统 prompt + slim evidence
│ ├── rules/{api,mcp}.ts # 本地规则 flag(DSL 校验 + trajectory 卫生)
│ ├── rl-env.ts # buildTaskPackage()
│ └── zip-export.ts # 写出最终 *.task-package.zip
├── data/ # Docker volume:SQLite + 上传/导出(已 .gitignore)
├── docs/ # 长文档/设计资料(新增文档优先放这里)
├── Dockerfile # 多阶段 node:20 + pnpm,构建 web/core/api
├── docker-compose.yml
├── .env.example
└── pnpm-workspace.yaml
```
`slack-cli/``discord-cli/` 是独立 npm 子项目,不在本工程范围内。
### 本地目录卫生
- **源码只放这些地方**`apps/``packages/`、根目录配置文件、`README.md`。新增长文档优先放 `docs/`,避免根目录继续变胖。
- **运行数据只放 `data/`、`.data/` 或 `.local-data/`**:上传 ZIP、导出包、SQLite 文件都属于本地运行状态,已被 `.gitignore` 忽略。
- **构建产物不入库**`apps/api/dist/``packages/core/dist/``coverage/``.vitest/``tmp/` 都是可再生成内容。
- **本地私密配置不入库**`.env``.claude/` 保持本机私有;提交前只看 `.env.example`
- **清理命令**:用 `pnpm clean` 删除 TypeScript 构建产物;系统垃圾如 `.DS_Store``nul``%USERPROFILE%*` 已在 `.gitignore` 中兜底。
---
## 快速启动
需要 Docker(Desktop 或 Engine)。**不再依赖** macOS `start.command`、Python static server 或浏览器 Babel/CDN。
```bash
git clone <repo>
cd Rl-Auto
cp .env.example .env
# 编辑 .env:填入 DEFAULT_API_KEY / JUDGE_API_KEY / RUBRIC_GEN_API_KEY
docker compose up --build
```
首次构建 3-5 分钟(拉镜像 + pnpm 装依赖 + 编译 TS)。后续 `docker compose up` 秒级。
启动后访问:
- 前端:<http://localhost:3001/>
- 健康检查:`curl http://localhost:3001/api/health``{"code":0,"msg":"ok",…}`
### 本地开发(绕开 Docker)
```bash
pnpm install
pnpm build
mkdir -p .local-data/uploads .local-data/exports
SQLITE_PATH=$PWD/.local-data/tasks.db \
UPLOAD_DIR=$PWD/.local-data/uploads \
EXPORT_DIR=$PWD/.local-data/exports \
PORT=3001 pnpm dev:api # tsx watch,改 TS 自动重启
```
前端是 Vite 构建产物,由 Fastify 从 `apps/web-legacy/dist/` 托管。修改前端源码后重新运行 `pnpm --filter @task-optimizer/web-legacy build`,再刷新浏览器。
---
## 配置说明
配置分两类:**后端 `.env`(部署时设定)****前端设置面板(用户运行时改)**。API/MCP 分组启动 job 时会随 `optionsSnapshot` 发送前端值,主模型 Key 留空则回退到 `.env` 默认值;首页任务润色固定浏览器直连,必须使用前端填写或个人设置保存的 Key。
### 后端 `.env`
| 变量 | 说明 | 默认 |
|------|------|------|
| `PORT` | 监听端口 | `3001` |
| `LOG_LEVEL` | pino 日志级别 | `info` |
| `DEFAULT_MODEL` | 主分析模型默认值 | `deepseek-v4-pro` |
| `DEFAULT_BASE_URL` | 主分析模型默认 base URL | `https://api.deepseek.com` |
| `DEFAULT_API_KEY` | 主分析模型密钥(**前端可覆盖**) | *(必填)* |
| `DEFAULT_API_MODE` | `chat``responses` | `chat` |
| `JUDGE_MODEL` / `JUDGE_BASE_URL` / `JUDGE_API_KEY` / `JUDGE_API_MODE` | Judge 模型配置(**只在后端**) | `gpt-5.5` / `https://app-hk.ppapi.ai/v1/chat/completions` |
| `RUBRIC_GEN_*` | Rubric 自动补全模型(后端专用) | `gpt-5.5` / `https://app-hk.ppapi.ai/v1/chat/completions` |
| `STORE_BACKEND` | `sqlite``postgres`;多实例部署必须使用 `postgres` | `sqlite` |
| `DATABASE_URL` | PostgreSQL 连接串;存在时后端自动使用 Postgres store | *(空)* |
| `SQLITE_PATH` | SQLite 路径(仅本地/单实例推荐) | `/app/data/tasks.db` |
| `UPLOAD_DIR` / `EXPORT_DIR` | 上传 / 导出目录 | `/app/data/{uploads,exports}` |
| `MAX_UPLOAD_MB` | 单文件最大尺寸 | `100` |
| `MAX_BATCH_FILES` | 单批最大文件数 | `20` |
| `ITEM_MAX_ATTEMPTS` | 单个 item 的 transient 失败自动重试上限 | `3` |
| `GLOBAL_LLM_CONCURRENCY` | 单进程 LLM 调用并发上限;`0` 表示不额外限制 | `0` |
| `UPSTREAM_PROXY` | 出站代理 URI(替代旧 `proxy.mjs`),如 `socks5://127.0.0.1:1080``http://...` | *(空)* |
### 前端设置面板(持久化在浏览器 localStorage)
| 字段 | 说明 |
|------|------|
| `ruleProfile` | `echo-05061347` / `rl-env-4.30` / `echo-04281753` |
| `polishTransport` | 首页任务润色调用路径;当前固定为 `browser` 浏览器直连 |
| `analysisTransport` | API/MCP 分组路径;当前固定为 `backend` 队列 |
| `apiMode` | `chat` / `responses` |
| `baseURL` | 主分析模型 endpoint |
| **`apiKey`** | 浏览器直连 API Key |
| `savedModelCredentials` / `selectedModelCredentialId` | 个人保存的 Base URL + Key 列表,以及当前选中的连接 |
| `modelPreset` / `customModel` | 主模型名 |
| `concurrency` | 单 job 内并发 worker 数(1-8) |
| `temperature` / `topP` / `maxTokens` | 主模型采样参数 |
| `enableThinking` / `clearThinking` | 主模型 thinking 行为开关 |
| `optimizeTask` / `feedback` | 任务润色开关 + 用户反馈文本 |
| `strictFullMarks` | 严格满分开关(影响 pass policy) |
| `ignoreTaskComplexityForFullMarks` | 严格满分时允许 `TASK COMPLEXITY=1/2`,只要求其他 5 个 Judge 维度满分 |
| `judge.{baseURL, model, promptTemplate}` | Judge 配置(**仅 baseURL/model/template 可前端改;apiKey 仍只在 .env**) |
| `rubricGeneration.{baseURL, model}` | Rubric 自动补全配置(同上) |
> **关于密钥与 localStorage**:首页任务润色固定走 `browser` 直连,必须填写浏览器 API Key,或在个人设置里选择已保存的 Base URL + Key。主模型 `apiKey` 和 `savedModelCredentials` 会按用户操作保存到浏览器 localStorage。API/MCP 分组默认走后端队列。Judge / Rubric Generation 的 apiKey 仍然只在后端 `.env`,从不出现在前端代码或响应里。
---
## 后端 API 契约
所有响应使用统一 envelope:
```json
{ "code": 0, "msg": "ok", "data": { /* ... */ } }
```
非 200 时 `code` 为非 0 整数,`msg` 描述错误。
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/api/health` | 健康检查 |
| `POST` | `/api/polish` | 首页任务润色接口;body 为 `{ raw, options? }`,返回 `{ zh, en, notes }` |
| `POST` | `/api/analysis/jobs` | multipart 上传 1..N 个 `.zip`,自动识别 mode;返回 `{ jobId, items[] }` |
| `GET` | `/api/analysis/jobs/:jobId` | 当前 job + 所有 item 状态 |
| `POST` | `/api/analysis/jobs/:jobId/start` | 开始处理;body 含 `optionsSnapshot` |
| `POST` | `/api/analysis/jobs/:jobId/cancel` | 当前 in-flight item 跑完后停后续 |
| `POST` | `/api/analysis/jobs/:jobId/items/:itemId/retry` | 重置单项为 queued 重新跑 |
| `GET` | `/api/analysis/jobs/:jobId/items/:itemId/download` | 流式下载该项 `*.task-package.zip` |
| `GET` | `/api/analysis/jobs/:jobId/download-all` | 流式打包所有成功项 → `all-results-{jobId}.zip` |
- **Item status**: `queued | running | judging | completed | failed | stopped`
- **Job status**: `pending | running | completed | cancelled`
---
## 处理流水线
每次上传 ZIP → 一个 item,每个 item 流程如下:
```
parse (importer) ← packages/core/importers/{api,mcp}.ts
buildEvidence + runLocalRules ← packages/core/rules/{api,mcp}.ts
┌─────────────────────────────────────────────────────────┐
│ for iter = 1..MAX_JUDGE_ITERATIONS (=5): │
│ buildSlimEvidence + buildUserPrompt │
│ ↓ (上一轮 judge 反馈拼进 prompt) │
│ 主模型 requestJson → analyzerResult │
│ ↓ │
│ buildTaskPackage (含 golden_trajectory) │
│ ↓ │
│ runJudge → 6 维评分 + verdict │
│ ↓ │
│ isJudgeTargetSatisfied? │
│ ✓ → break (PASS) │
│ ✗ → 更新迭代记忆,下一轮带着累计修正继续 │
│ │
│ 跟踪 best score;每轮快照 evidence/task-package/judge │
│ 落盘到 data/exports/{jobId}/{itemId}/ │
└─────────────────────────────────────────────────────────┘
↓ (PASS) → buildAnalysisZipBuffer → output.zip
↓ (5 轮仍未通过) → 写入 best 那次产物 + 失败信息
```
---
## Judge 迭代
`apps/api/src/queue/runner.ts` 内每个 item 最多跑 **`MAX_JUDGE_ITERATIONS = 5`** 轮。每轮:
1. 把上一轮 `judgeResult.rationale` + 0/1 分维度的 explanation 拼成 `=== JUDGE FEEDBACK ===`
2. 后端迭代记忆模块会合并重复问题、清理已修复维度,并生成下一轮累计反馈
3.`optionsSnapshot.feedback`(用户输入)合并塞进下一轮 user prompt
4. 模型据此修订 `recommended_groups` / `recommended_rubrics` / `recommended_instruction`
通过条件(`isJudgeTargetSatisfied`):
- **默认**`total_score >= 10 AND has_zeros === false` → PASS
- **`strictFullMarks` 开启**`total_score === sum(rubrics[].max_score) AND has_zeros === false` → PASS
- **`strictFullMarks` + `ignoreTaskComplexityForFullMarks` 开启**:除 `task_complexity` 外其他 5 个维度都为 `2/2`,且 `task_complexity >= 1/2` → PASS
满 5 轮仍未通过:用最高分那次的产物写持久化,抛错带 "已迭代 N/5 轮,最佳 X 分"。
每轮快照:`data/exports/{jobId}/{itemId}/{evidence,task-package,judge-result,iteration-memory}-iter-N.json`,便于追溯。
---
## 并发模型
```
Job (上传 5 个 ZIP)
├─ Worker #1 ─→ takeNextQueuedItem() → item A → 5 轮 judge 迭代 → export
├─ Worker #2 ─→ takeNextQueuedItem() → item B → 5 轮 judge 迭代 → export
├─ Worker #3 ─→ takeNextQueuedItem() → item C → ...
└─ ... (worker 数 = optionsSnapshot.concurrency, 1-8)
```
- **per-job 内并发**`concurrency` 个 worker 并行 drain 同一 job 的 queue。SQLite 本地模式使用事务原子抢取;Postgres 多实例模式使用 `FOR UPDATE SKIP LOCKED`,避免不同 API 实例重复消费同一个 item。
- **跨 job 并发**:旧版有 `workerLock` 跨 job 串行链,已撤销。多用户/多次上传可同时跑。
- **硬上限**`MAX_CONCURRENCY = 8``apps/api/src/queue/runner.ts`),保护上游模型 API rate limit。
- **LLM 全局上限**`GLOBAL_LLM_CONCURRENCY` 可限制单进程内 analyzer/judge 同时调用模型的数量。
- **失败重试**`ITEM_MAX_ATTEMPTS` 只重试 transient AI/network 错误;ZIP 格式错误、严格 MCP tools 缺失、Judge 质量未达标会直接 failed。
- **配置**:前端"并发数"输入框(设置面板),默认 3。
---
## MCP 严格 trajectory 卫生
为避免 Judge 在 Trajectory-Task Alignment 维度扣分(典型问题:跨 channel 的 list_messages、`start_typing` 噪音、`get_unread` 出现在 `mark_read` 之前),有三层防御:
### 第 1 层:本地规则 flag(`packages/core/src/rules/mcp.ts`)
`runMcpRules` 主动检测并 flag:
- `cross_entity_channelId` / `cross_entity_guildId` / `cross_entity_messageId` / ...:录制涉及多个不同实体 ID
- `unread_check_before_mark_read``get/list_unread` 出现在 `mark_*_read` 之前
- `noise_tools_present`:背景调用(typing/presence/heartbeat/auth/telemetry/...)
### 第 2 层:evidence 过滤(`isSignificantMcpCall`)
Slim evidence 阶段直接剔除噪音工具,模型甚至看不到:
```
login | logout | refresh_token | get_session | whoami | get_current_user | get_self | me | default_config
start_typing | stop_typing | set_typing | presence | heartbeat | ping
get_pending_* | list_pending_* | telemetry_* | page_load | page_view | init_* | bootstrap_*
*_typing_* | auth_* | session_* | *_telemetry | *_heartbeat | *_ping
```
### 第 3 层:System prompt 强约束(`MCP_TRAJECTORY_RULES`)
写进 `MCP_SYSTEM_PROMPT`,模型必须遵守:
1. **ENTITY CONSISTENCY**:所有 group + rubric 引用的 calls 必须共享同一个 channel/guild/message ID。识别主线实体 = 最多写操作命中的那个 ID。
2. **NOISE BLACKLIST**:明确列出禁用工具,不能进 `recommended_groups.calls``checker_key`
3. **ORDER CONSTRAINTS**
- `mark_*_read` 必须在 `get_unread_*` / `list_unread_*` 之前
- `create_*` 必须在 `list_*` / `get_*` 验证调用之前
- `send_message` 必须在 `list_messages` / `search_*_messages` 之前
4. **STRAY-CALL REJECTION**:违反以上的 stray 调用一律剔除,可记入 `evidence_limits`
---
## 输出 ZIP 格式
输出 ZIP 与上传 ZIP **完全同结构**,仅 `task.json` 被重写为含完整任务包字段:
- 顶层保留 echo-extension 兼容字段:`metadata``task.instruction``task.networkRequests``task.subtasks``task.rubrics`
- 顶层嵌入 RL 字段:`rule_profile``rubric_checkers``dbdiff_criteria``pass_policy``golden_trajectory``network``validation`
- API 模式:`task.subtasks[*].groupMode === "api"``apiCalls` 是完整对象(`{ id, name, type, input, output, metadata }`
- MCP 模式:`task.subtasks[*].groupMode === "mcp"`,tool name 严格保留
- `network.json` / `mcp_calls.json` / `session.json` / `dbdiff.json` 等按原始字节保留
---
## 数据持久化
`./data/` 是 Docker volume mount 目标:
```
data/
├── tasks.db # SQLite jobs/items
├── uploads/{jobId}/{itemId}.zip # 上传原始
└── exports/{jobId}/{itemId}/
├── evidence.json # 最终(best)证据
├── task-package.json # 最终任务包
├── judge-result.json # 最终 judge 结果
├── evidence-iter-N.json # 每轮迭代快照
├── task-package-iter-N.json
├── judge-result-iter-N.json
├── output.zip # 仅 PASS 时生成
└── error.txt # 失败时
```
清理某个 job 的所有数据:
```bash
docker compose down
rm -rf ./data/uploads/<jobId> ./data/exports/<jobId>
sqlite3 ./data/tasks.db "DELETE FROM items WHERE job_id='<jobId>'; DELETE FROM jobs WHERE id='<jobId>';"
docker compose up
```
---
## 验收清单
1. `docker compose up --build` 一条命令启动成功
2. `http://localhost:3001` UI 显示正常
3. 单 API ZIP → 分析 → 下载 `*.task-package.zip``groupMode: "api"` + `apiCalls` 完整对象
4. 单 MCP ZIP → 同上:`groupMode: "mcp"`,tool name 严格保留,无跨实体 / 噪音 / 顺序问题
5. 一次拖 N 个混合 ZIP → 按 `concurrency` 并发处理,可中途取消
6. 杀容器再 `docker compose up` → 队列从 SQLite 恢复,未完成 item 状态可见
7. 主模型 apiKey:前端填入或选择个人保存的 Base URL + Key → 浏览器直连立即生效
8. devtools Network 抓包:首页任务润色直接命中所选模型 Base URL;API/MCP 分组只命中 `localhost:3001/api/*`
9. 严格满分开关:开启 → 须 `total = max_score`;关闭 → `total >= 10`;同时开启“忽略 Complexity”时允许 `TASK COMPLEXITY=1/2`
10. Judge 失败时自动迭代下一轮,最多 5 轮,每轮都会更新后端迭代记忆并写进下轮 prompt(容器日志可见 `Iteration start` / `Judge target not yet satisfied`
---
## 故障排查
| 现象 | 排查 |
|------|------|
| `docker compose up` 卡在装 `better-sqlite3` | Dockerfile 已含 native deps;本地 `pnpm install` 失败需先装 Xcode CLT (`xcode-select --install`) |
| `/api/health` 返回 502 / 连接拒绝 | `docker compose ps` 看 service 状态;`docker compose logs -f app` 查启动日志 |
| 大 ZIP 上传 413 | 调大 `MAX_UPLOAD_MB` 后重启容器 |
| AI 调用失败 401 / 429 | 检查相应 KEY;后端日志会用安全 preview 显示模型返回(自动屏蔽 `Authorization` / `apiKey`) |
| Judge 一直 NEEDS_REVISION 跑满 5 轮 | 看 `data/exports/{jobId}/{itemId}/judge-result-iter-N.json` 和 `iteration-memory-iter-N.json`,找出反复失败的维度;考虑改 `optionsSnapshot.feedback` 给模型更针对性的指令 |
| MCP Trajectory-Task Alignment 仍 ≤ 1 | 检查录制是否真有跨 channel 的杂讯 / 噪音工具未被剔除(`evidence-iter-N.json` 里 `slim.callInstances`) |
| 主模型 apiKey 改了不生效 | 浏览器刷新;确认设置面板里 "浏览器 API Key" 输入框值已保存,或重新选择个人设置里的连接 |
---
## 设计决策与历史
- **退役 `proxy.mjs`**:原本浏览器直连模型 endpoint 通过 Node CORS proxy 中转。后端化之后,所有 AI 调用走 `apps/api/src/ai/client.ts`,不需要 CORS。如需出站代理(公司网络)用 `UPSTREAM_PROXY` 环境变量(基于 undici `ProxyAgent`)。
- **Pass policy 阈值从 9 升到 10**:旧 echo-extension Judge prompt 里 PASS=9,但实际经验下 9 分包仍有较多噪音。运行时由后端 `isJudgeTargetSatisfied` 决定,模型 prompt 不变。
- **iteration 循环搬到后端**:原先在 vanilla JS 前端做;后端化后,前端只负责 polling 状态,循环完整运行在 `runner.ts`
- **主模型 apiKey 前端化**:早期为防止 localStorage 泄漏,所有 apiKey 都不允许前端持有。现在主模型 key 已开放(用户自主选择),Judge / Rubric Generation 的 key 仍后端独控。
---
## 许可
私有项目,未开源。