940ec29398
Pre-existing uncommitted work (auth dialog, provider profiles editor, runs route) preserved as baseline.
546 lines
18 KiB
Markdown
546 lines
18 KiB
Markdown
# WebUI 三种审核模式修改方案
|
||
|
||
> 版本:3.1 | 日期:2026-07-17 | 所属项目:EvoScientist-WebUI / EvoScientist
|
||
> 状态:单用户最小方案,已实施
|
||
|
||
## 1. 目标
|
||
|
||
本功能让单用户在 WebUI 中为当前对话选择大模型的操作审核方式。它不是用户权限、
|
||
后台授权或多用户审批系统。
|
||
|
||
| WebUI 模式 | 内部值 | 受现有 HITL 管控的工具 | `ask_user` |
|
||
| ------------- | -------- | ---------------------- | ------------------ |
|
||
| Manual review | `manual` | 用户逐次审核 | 允许并等待用户 |
|
||
| Auto-approve | `auto` | WebUI 自动 approve | 允许并等待用户 |
|
||
| Full approve | `full` | WebUI 自动 approve | 禁用,模型自行判断 |
|
||
|
||
核心定义:
|
||
|
||
```text
|
||
Full approve = Auto-approve + 不使用 ask_user
|
||
```
|
||
|
||
Auto 和 Full 的工具处理完全相同。唯一差异是 Auto 允许模型通过 `ask_user` 提问,
|
||
Full 不允许模型等待用户回答,信息不足时由模型采用合理、保守的假设继续。
|
||
|
||
## 2. 工具审核范围
|
||
|
||
本次严格沿用当前 `HumanInTheLoopMiddleware` 的范围:
|
||
|
||
```text
|
||
execute
|
||
run_in_background
|
||
schedule_task
|
||
```
|
||
|
||
其他普通工具和 MCP 工具当前不会产生 HITL interrupt,三种模式都不改变它们的
|
||
执行方式。本次不扩展 HITL 工具范围。
|
||
|
||
现有 Auto-approve 流程保持不变:
|
||
|
||
```text
|
||
模型调用受审核工具
|
||
-> 后端产生 action_requests interrupt
|
||
-> 当前打开的 ChatInterface 读取 interrupt
|
||
-> Auto 或 Full 自动提交 approve decisions
|
||
-> WebUI 创建 resume run
|
||
-> 后端继续执行工具
|
||
```
|
||
|
||
因此 Full 不是新的服务端无人值守模式。页面关闭或线程未打开时,工具 interrupt
|
||
仍然停留在 Requiring Attention;用户打开线程后才触发前端自动恢复。
|
||
|
||
## 3. 设计原则与边界
|
||
|
||
### 3.1 本次实现
|
||
|
||
- WebUI 提供 Manual、Auto、Full 三种模式。
|
||
- 模式按线程保存在浏览器 `localStorage`。
|
||
- Manual 不自动处理工具 approval interrupt。
|
||
- Auto 和 Full 复用现有前端自动 approve effect。
|
||
- Manual 和 Auto 保留 `ask_user`。
|
||
- Full 从模型工具列表中移除 `ask_user`,并增加工具侧防御。
|
||
- 新 run 和 resume run 都显式携带 `review_mode`。
|
||
- 运行、重连或线程加载时禁止切换;工具 approval interrupt 暂停后允许选择模式。
|
||
- 未知、损坏或缺失的模式统一降级为 Manual。
|
||
|
||
### 3.2 本次不实现
|
||
|
||
- 不修改现有 run discovery、断线恢复和游标机制。
|
||
- 不新增 `resume_interrupt_key`、新的 resume 幂等协议或自动重试状态机。
|
||
- 不新增审批数据库、Admin Token、审批 API、用户、角色、ACL 或多租户权限。
|
||
- 不新增 Assistant 或 LangGraph graph。
|
||
- 不修改 Scheduler 审计模型。
|
||
- 不修改 `dangerous_mode`、工作区沙箱或命令 blocklist。
|
||
- 不修改 Stop、Token 统计及现有 `turn_id` 语义。
|
||
- 不把 `review_mode` 当作安全授权边界。
|
||
- 不保证同一线程被多个浏览器标签页同时操作时的模式一致性。
|
||
|
||
该设计面向当前单用户、本机可信部署。运行恢复本身的幂等增强属于独立问题,不能
|
||
为了新增审核模式而扩大本次改动范围。
|
||
|
||
## 4. WebUI 数据模型
|
||
|
||
### 4.1 类型与辅助函数
|
||
|
||
新增 `src/lib/reviewMode.ts`:
|
||
|
||
```ts
|
||
export type ReviewMode = "manual" | "auto" | "full";
|
||
|
||
export const DEFAULT_REVIEW_MODE: ReviewMode = "manual";
|
||
|
||
export function autoApprovesTools(mode: ReviewMode): boolean {
|
||
return mode === "auto" || mode === "full";
|
||
}
|
||
|
||
export function suppressesAskUser(mode: ReviewMode): boolean {
|
||
return mode === "full";
|
||
}
|
||
```
|
||
|
||
该模块同时负责模式解析、线程存储、新线程迁移和旧 Auto-approve 数据迁移。所有
|
||
读取结果都必须经过枚举校验,非法值返回 `manual`。
|
||
|
||
### 4.2 浏览器存储
|
||
|
||
新增键:
|
||
|
||
```text
|
||
evoscientist-review-mode
|
||
```
|
||
|
||
结构:
|
||
|
||
```json
|
||
{
|
||
"thread-id-1": "auto",
|
||
"thread-id-2": "full",
|
||
"__new__": "auto"
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- 每个 thread ID 保存自己的模式。
|
||
- New Chat 使用现有 `__new__` sentinel 思路。
|
||
- 第一次发送消息并创建真实线程后,将 sentinel 迁移到真实 thread ID。
|
||
- 用户从 New Chat 直接打开已有线程时,清除未使用的 sentinel,避免模式泄漏。
|
||
- Manual 是默认值,不保存 Manual 条目,缺失即表示 Manual。
|
||
- localStorage 不可用、写满或内容损坏时退回 Manual,不影响聊天功能。
|
||
|
||
### 4.3 旧数据迁移
|
||
|
||
旧键:
|
||
|
||
```text
|
||
evoscientist-auto-approve
|
||
```
|
||
|
||
迁移规则:
|
||
|
||
```text
|
||
旧值不存在或 false -> manual
|
||
旧值 true -> auto
|
||
```
|
||
|
||
旧值不能迁移成 Full。迁移成功后删除对应旧条目,兼容读取保留一个版本。
|
||
|
||
## 5. 前端状态与调用接口
|
||
|
||
### 5.1 状态所有权
|
||
|
||
继续沿用当前实现,把线程审核模式保存在 `ChatInterface`:
|
||
|
||
```ts
|
||
const [reviewMode, setReviewModeState] = useState<ReviewMode>(() =>
|
||
getThreadReviewMode(threadId)
|
||
);
|
||
```
|
||
|
||
线程切换时重新读取对应模式。模式菜单通过统一 setter 同时更新 React state 和
|
||
localStorage。
|
||
|
||
该方案不把模式状态迁入 `ChatProvider`,也不新增 active-turn state/ref。单次提交
|
||
通过显式函数参数冻结模式,而不是依赖异步回调重新读取 UI state。
|
||
|
||
### 5.2 模式锁定
|
||
|
||
以下任一条件成立时禁用模式菜单:
|
||
|
||
```text
|
||
isLoading == true
|
||
isReconnecting == true
|
||
isThreadLoading == true
|
||
```
|
||
|
||
工具 approval interrupt 表示前一个 run 已经安全暂停,此时菜单保持可用。用户可在
|
||
审批卡片选择 `Approve once`,也可选择 `Auto-approve this chat`;后者在同一次操作中
|
||
保存线程模式并用 Auto 创建 resume run。刷新后从当前线程 localStorage 恢复模式;
|
||
如果存储不可用或非法,按 Manual 处理,这是保守降级。
|
||
|
||
该最小方案不处理多个浏览器标签页同时打开并修改同一线程模式的情况。另一个标签
|
||
页修改 localStorage 后再刷新当前页面,可能使后续 resume 使用新的线程模式。当前
|
||
单用户部署接受该限制,不为此重新引入 active-turn 状态或历史 run 扫描。
|
||
|
||
### 5.3 显式传递模式
|
||
|
||
修改 `useChat` 的内部调用签名:
|
||
|
||
```ts
|
||
sendMessage(content, reviewMode);
|
||
resumeInterrupt(value, reviewMode);
|
||
startBackgroundRun({ input, command, turnId, reviewMode });
|
||
buildRunConfig(reviewMode);
|
||
```
|
||
|
||
`ChatInterface` 调用 `sendMessage()` 和 `resumeInterrupt()` 时传入当前 `reviewMode`。
|
||
`startBackgroundRun()` 只使用参数值构建 run,不在异步执行时重新读取组件状态。
|
||
|
||
`ChatInterface` 分别创建普通 resume 和审批时切换模式的包装函数:
|
||
|
||
```ts
|
||
const resumeWithReviewMode = useCallback(
|
||
(value: unknown) => resumeInterrupt(value, reviewMode),
|
||
[resumeInterrupt, reviewMode]
|
||
);
|
||
|
||
const resumeWithApprovalMode = useCallback(
|
||
(value: unknown, nextMode?: ReviewMode) => {
|
||
const mode = nextMode ?? reviewMode;
|
||
if (nextMode) setThreadReviewMode(threadId, nextMode);
|
||
resumeInterrupt(value, mode);
|
||
},
|
||
[resumeInterrupt, reviewMode, threadId]
|
||
);
|
||
```
|
||
|
||
ask-user handlers 和自动 approve effect 使用普通包装函数。工具审核组件使用
|
||
`onResumeInterrupt(value, nextMode?)`,让审批卡片可以原子地切换模式并恢复。实现
|
||
必须先占用当前 interrupt 的自动恢复锁,避免按钮提交与 Auto effect 重复创建 run。
|
||
|
||
`startBackgroundRun()` 必须分离 thread metadata 和 run metadata:
|
||
|
||
```ts
|
||
const baseMetadata = {
|
||
usage_context_version: 1,
|
||
turn_id: turnId,
|
||
};
|
||
|
||
const runMetadata = {
|
||
...baseMetadata,
|
||
review_mode: reviewMode,
|
||
};
|
||
```
|
||
|
||
- `client.threads.create()` 只使用 `baseMetadata`。
|
||
- `client.runs.create()` 使用 `runMetadata`。
|
||
- 不把 `review_mode` 写入 thread metadata;线程模式的唯一来源仍是 localStorage。
|
||
|
||
新 run 和 resume run 的请求为:
|
||
|
||
```json
|
||
{
|
||
"config": {
|
||
"configurable": {
|
||
"review_mode": "full"
|
||
}
|
||
},
|
||
"metadata": {
|
||
"review_mode": "full",
|
||
"turn_id": "existing-logical-turn-id"
|
||
}
|
||
}
|
||
```
|
||
|
||
要求:
|
||
|
||
- `buildRunConfig(reviewMode)` 合并现有 configurable,不能覆盖模型配置。
|
||
- run metadata 中的模式仅用于日志和排查。
|
||
- resume run 继续使用现有 `turn_id` 和现有 run 创建/恢复逻辑。
|
||
- 不从历史 run metadata 重新计算 WebUI 模式。
|
||
|
||
## 6. WebUI 交互修改
|
||
|
||
### 6.1 模式菜单
|
||
|
||
把当前 Auto-approve boolean 按钮替换为紧凑的三段图标控件:
|
||
|
||
- `Hand`:Manual review
|
||
- `Zap`:Auto-approve
|
||
- `ChevronsRight`:Full approve
|
||
|
||
激活项以颜色高亮,悬停提示完整模式名称和行为说明;控件使用 `radiogroup`
|
||
语义和 `aria-label`,不依赖文字表达。切换 Auto 或 Manual 后显示一次 toast,
|
||
不再常驻占用输入区的警示横幅。选择 Full 时显示确认对话框,说明模型将不再通过
|
||
`ask_user` 等待输入,而会使用合理假设继续。
|
||
|
||
### 6.2 工具自动 approve
|
||
|
||
现有 effect 的条件从:
|
||
|
||
```ts
|
||
if (!autoApprove) return;
|
||
```
|
||
|
||
改为:
|
||
|
||
```ts
|
||
if (!autoApprovesTools(reviewMode)) return;
|
||
```
|
||
|
||
approve payload 和现有去重机制保持不变:
|
||
|
||
```ts
|
||
resumeWithReviewMode({
|
||
decisions: actionRequests.map(() => ({ type: "approve" })),
|
||
});
|
||
```
|
||
|
||
Manual 不执行该 effect;Auto 和 Full 执行。
|
||
|
||
### 6.3 展示组件
|
||
|
||
`ChatInterface` 派生:
|
||
|
||
```ts
|
||
const toolsAutoApproved = autoApprovesTools(reviewMode);
|
||
```
|
||
|
||
继续通过现有 `autoApprove` boolean prop 控制展示。审批回调增加可选的
|
||
`nextReviewMode`;卡片提供 `Approve once` 和 `Auto-approve this chat`。同一 interrupt
|
||
含多个工具时,切换 Auto 会保留已经明确做出的 Reject/Edit,只为未决定项补
|
||
Approve,然后只提交一次 resume。
|
||
|
||
### 6.4 ThreadList
|
||
|
||
- Manual 的工具 interrupt 需要用户关注。
|
||
- Auto 和 Full 的当前打开线程会自动恢复工具 interrupt。
|
||
- 后台或未打开线程中的工具 interrupt 仍显示 Requiring Attention。
|
||
- Manual 和 Auto 的 `ask_user` 需要用户关注。
|
||
|
||
`ThreadList` 将 `getThreadAutoApprove()` 替换为:
|
||
|
||
```ts
|
||
autoApprovesTools(getThreadReviewMode(thread.id));
|
||
```
|
||
|
||
### 6.5 Full 的异常 ask_user fallback
|
||
|
||
正常部署下,Full run 不会产生 `ask_user` interrupt。为兼容服务更新前已经持久化
|
||
的 interrupt 或前后端短暂版本不一致,WebUI 保留轻量 fallback:
|
||
|
||
```text
|
||
interrupt.type == ask_user && reviewMode == full
|
||
-> 为每个问题填入统一回答
|
||
-> 使用现有 resumeInterrupt() 恢复
|
||
```
|
||
|
||
统一回答:
|
||
|
||
```text
|
||
请根据当前上下文采用合理、保守的假设继续,无需等待用户确认,并在最终结果中说明关键假设。
|
||
```
|
||
|
||
fallback 使用独立的 ask-user interrupt key/ref 防止重复提交。key 的优先级固定为:
|
||
|
||
```text
|
||
1. ask-user:<interrupt.id>
|
||
2. ask-user:<interrupt.value.tool_call_id>
|
||
3. ask-user:<stableStringify(ns + scope + questions)>
|
||
```
|
||
|
||
规则:
|
||
|
||
- 检测到 ask-user interrupt 后先生成 key;与 ref 相同则不再次提交。
|
||
- 首次提交前把 key 写入 ref,防止 React effect 重复执行。
|
||
- interrupt 消失、线程切换或出现不同 key 时清理旧 ref。
|
||
- key 必须带 `ask-user:` 命名空间,不能复用依赖 `action_requests` 的工具审批 key。
|
||
- resume 创建失败时沿用现有错误提示和断线恢复行为;刷新或重新打开线程会重新
|
||
挂载组件并获得一次新的 fallback 机会。
|
||
|
||
除为 `resumeInterrupt()` 增加显式 `reviewMode` 参数外,不修改其返回值、run 创建
|
||
流程或恢复语义,也不增加自动重试和新的幂等协议。
|
||
|
||
## 7. EvoScientist 修改
|
||
|
||
### 7.1 模式解析
|
||
|
||
在 `EvoScientist/middleware/ask_user.py` 增加:
|
||
|
||
```python
|
||
def _review_mode() -> str:
|
||
try:
|
||
config = get_config()
|
||
except Exception:
|
||
return "manual"
|
||
|
||
if not isinstance(config, dict):
|
||
return "manual"
|
||
configurable = config.get("configurable") or {}
|
||
if not isinstance(configurable, dict):
|
||
return "manual"
|
||
|
||
mode = configurable.get("review_mode")
|
||
return mode if mode in {"manual", "auto", "full"} else "manual"
|
||
```
|
||
|
||
该实现与现有 `configurable_model.py` 的读取方式一致。外部 runnable context、异常、
|
||
malformed config 和非法模式均降级为 Manual。
|
||
|
||
### 7.2 Full 不暴露 ask_user
|
||
|
||
修改 `AskUserMiddleware.wrap_model_call()` 和 `awrap_model_call()`:
|
||
|
||
- Manual、Auto:保持现有 ask-user system prompt 和工具列表。
|
||
- Full:不注入 ask-user prompt;从 `request.tools` 移除名为 `ask_user` 的工具;
|
||
追加无人值守提示。
|
||
|
||
```text
|
||
You are running in Full approve mode. Do not wait for user clarification. When information is missing, make reasonable, conservative assumptions, continue the task, and report material assumptions in the final response.
|
||
```
|
||
|
||
工具过滤同时支持 `BaseTool` 和 dict tool schema,并通过
|
||
`request.override(system_message=..., tools=...)` 创建新请求,不直接修改原请求。
|
||
|
||
### 7.3 工具侧防御
|
||
|
||
即使模型异常生成 `ask_user` tool call,Full 也不能进入 `interrupt()`。在
|
||
`_ask_user` 进入问题校验和 interrupt 前检查模式;Full 直接返回:
|
||
|
||
```python
|
||
Command(
|
||
update={
|
||
"messages": [
|
||
ToolMessage(
|
||
"Full approve mode: continue with reasonable assumptions and do not ask the user again.",
|
||
tool_call_id=tool_call_id,
|
||
)
|
||
]
|
||
}
|
||
)
|
||
```
|
||
|
||
Manual 和 Auto 继续使用现有校验、interrupt 和答案解析流程。
|
||
|
||
## 8. 部署前提
|
||
|
||
三种模式依赖 AskUser 和 HITL middleware 在 graph 构建时已经注册。部署的最终有效
|
||
配置必须为:
|
||
|
||
```text
|
||
auto_approve = false
|
||
auto_mode = false
|
||
enable_ask_user = true
|
||
dangerous_mode = false
|
||
```
|
||
|
||
启动 `evoscientist deploy` 前检查:
|
||
|
||
```bash
|
||
uv run EvoSci config get auto_approve
|
||
uv run EvoSci config get auto_mode
|
||
uv run EvoSci config get enable_ask_user
|
||
uv run EvoSci config get dangerous_mode
|
||
```
|
||
|
||
CLI 预期显示 `False`、`False`、`True`、`False`。配置修改后必须重启 deploy 服务。
|
||
本次不增加 capability API 或 deploy fail-fast 逻辑。
|
||
|
||
## 9. 预计修改文件
|
||
|
||
### EvoScientist-WebUI
|
||
|
||
- `src/lib/reviewMode.ts`
|
||
- `src/lib/autoApprove.ts`:删除或保留一个版本的迁移 shim
|
||
- `src/app/components/ChatInterface.tsx`
|
||
- `src/app/components/ThreadList.tsx`
|
||
- `src/app/hooks/useChat.ts`
|
||
- 对应 Vitest 测试
|
||
|
||
### EvoScientist
|
||
|
||
- `EvoScientist/middleware/ask_user.py`
|
||
- `tests/test_ask_user.py`
|
||
|
||
明确不修改:
|
||
|
||
- `src/lib/runRecovery.ts`
|
||
- `src/providers/ChatProvider.tsx`
|
||
- `src/app/components/ChatMessage.tsx`
|
||
- `src/app/components/ActionGroup.tsx`
|
||
- `src/app/components/ToolCallBox.tsx`
|
||
- `EvoScientist/EvoScientist.py`
|
||
- `EvoScientist/config/settings.py`
|
||
- `EvoScientist/deploy/server.py`
|
||
- Scheduler、Token 统计、Stop 和断线恢复模块
|
||
|
||
## 10. 测试方案
|
||
|
||
### 10.1 WebUI 单元测试
|
||
|
||
1. 缺失或非法模式解析为 Manual。
|
||
2. 旧 false/缺失迁移为 Manual,旧 true 迁移为 Auto。
|
||
3. New Chat sentinel 正确迁移到真实 thread ID。
|
||
4. Manual 不自动处理 `action_requests`。
|
||
5. Auto 和 Full 都生成现有 approve decisions。
|
||
6. Manual 和 Auto 不自动回答 `ask_user`。
|
||
7. Full 自动恢复异常或旧 `ask_user` interrupt,且同一 interrupt 不重复提交。
|
||
8. 新 run 和 resume run 的 config、run metadata 携带显式传入的 `review_mode`。
|
||
9. 创建新线程时,thread metadata 不包含 `review_mode`。
|
||
10. `buildRunConfig()` 保留现有模型 configurable。
|
||
11. ask-user fallback key 按 interrupt ID、tool call ID、稳定 payload 的顺序生成。
|
||
12. ask-user fallback 在 interrupt 消失、线程切换和 key 改变时正确清理 ref。
|
||
13. 运行、重连和线程加载时模式菜单禁用,approval interrupt 时可用。
|
||
14. ThreadList 对 Manual、Auto、Full 的关注状态判断正确。
|
||
15. 展示组件继续收到正确的 `autoApprove` boolean。
|
||
16. 审批时切换 Auto 只创建一次 resume,并保留同批次已有 Reject/Edit 决定。
|
||
|
||
### 10.2 EvoScientist 单元测试
|
||
|
||
1. 缺失、非法或 malformed config/configurable 降级为 Manual。
|
||
2. Manual 和 Auto 注入 ask-user prompt 并暴露工具。
|
||
3. Full 不注入 ask-user prompt,并从最终工具列表移除 `ask_user`。
|
||
4. Full 工具侧调用返回 ToolMessage,不调用 `interrupt()`。
|
||
5. Full 的同步和异步 wrapper 行为一致。
|
||
6. Manual 和 Auto 的现有问答、取消和错误解析测试不回归。
|
||
|
||
### 10.3 端到端测试
|
||
|
||
1. Manual 对三个现有 HITL 工具显示审核卡片。
|
||
2. Auto 在当前打开线程自动继续工具操作,`ask_user` 正常显示。
|
||
3. Full 在当前打开线程自动继续工具操作,且正常运行不出现 `ask_user`。
|
||
4. 三种模式在线程切换和刷新后保持。
|
||
5. Full 信息不足时继续执行并在结果中说明关键假设。
|
||
6. 后台 Auto/Full 线程的 interrupt 继续显示 Requiring Attention,打开后恢复。
|
||
7. Stop、断线续跑和 Token 统计行为无回归。
|
||
8. 部署配置不满足前提时,不进入三模式验收。
|
||
9. Manual 审批卡片可选择仅批准当前操作或切换当前会话为 Auto 后继续。
|
||
|
||
## 11. 验收标准
|
||
|
||
1. WebUI 可以选择 Manual review、Auto-approve、Full approve。
|
||
2. 新对话默认 Manual。
|
||
3. Manual 只审核现有 HITL 范围内的三个工具。
|
||
4. Auto 和 Full 使用相同的现有工具自动 approve 机制。
|
||
5. Auto 保留 `ask_user`。
|
||
6. Full 正常调用不暴露 `ask_user`,异常调用也不会进入 interrupt。
|
||
7. 模式按线程保存在 localStorage;新 run 和 resume run 都携带该模式,但 thread
|
||
metadata 不保存该模式。
|
||
8. 运行中不能切换模式;工具 interrupt 暂停后可以在审批过程中选择模式。
|
||
9. 不修改现有 run recovery、Stop、Token 统计、安全沙箱和命令 blocklist。
|
||
10. 不引入审批数据库、Admin Token、多用户权限或新的后端 API。
|
||
11. 文档明确同一线程多标签页同时操作不在一致性保证范围内。
|
||
|
||
## 12. 独立后续工作
|
||
|
||
以下问题有价值,但不属于审核模式功能,应另建方案和 PR:
|
||
|
||
- 为同一 `turn_id` 的多次 resume 增加 `resume_interrupt_key`。
|
||
- 让 `resumeInterrupt()` 返回 created、recovered、failed 结构化结果。
|
||
- 为 run 创建失败增加有界自动重试。
|
||
- 加强响应丢失场景下的 resume 幂等恢复。
|
||
|
||
拆分后,审核模式改动可以独立验证;run recovery 改进也能覆盖全部中断类型,而
|
||
不是只服务于 Full approve。
|