refactor(runtime): 运行态数据迁出源码树到 ~/.agents,日志默认关并加轮转,清理实验残留
将运行态数据(对话/用户工作区/日志等)默认迁出源码树到 ~/.agents/<mode>, 对齐 ~/.claude、~/.codex 惯例;同时为日志加上开关与轮转,并归档/清理仓库内 长期堆积的实验残留与误产生文件。 ## 路径变量重写(核心) - 重写 config/paths.py:运行态根按模式分流,host -> ~/.agents/host, 其它(默认 docker/web)-> ~/.agents/web。 - 三级解析优先级(高→低): 1) 具体目录变量 DATA_DIR / LOGS_DIR / USER_SPACE_DIR / API_USER_SPACE_DIR 2) 模式根变量 AGENTS_HOST_HOME / AGENTS_WEB_HOME 3) 兜底 ~/.agents/<mode> - 导出变量名保持不变,20+ 处消费方零改动;统一 config/sub_agent.py 重复的 路径解析逻辑,将 sub_agent 任务目录/状态文件收编进 DATA_DIR; SUB_AGENT_PROJECT_RESULTS_DIR 仍随工作区(有意保留)。 - config/*.json、prompts/、agentskills/ 属配置/资源,仍锚定源码树。 ## 日志止血 - 新增 utils/log_rotation.py:按大小轮转(默认 20MB×3 份)+ 按份清理 (dump 默认保留最近 30 个),阈值可由 AGENT_LOG_ROTATE_MAX_BYTES / AGENT_LOG_ROTATE_BACKUPS / AGENT_DUMP_KEEP 覆盖。 - utils/api_client.py:API 请求体 dump 默认关闭(AGENT_API_DUMP_ENABLED 开启), 收编两处写死的 logs/ 硬编码路径改用 LOGS_DIR;dump 关闭时 request_dump 安全置空。 - utils/host_workspace_debug.py、server/utils_common.py(chunk/conn_diag 等)、 utils/logger.py(TaskLogger/ErrorLogger)统一接入轮转。 ## 迁移与运维脚本 - scripts/migrate_runtime_data.py:源码树 -> 运行态根,复制+备份+校验+可回滚+幂等, logs 丢弃不迁;import config 复用程序同一套路径解析(模式由 .env 决定), 覆盖 data/users/api 及 sub_agent/tasks(-> data/sub_agent_tasks)。 - scripts/cleanup_misplaced_web.sh:清理误迁到 ~/.agents/web 的副本(带 chflags 兜底)。 ## 测试 - 重写 test/test_config_paths_resolution.py:覆盖默认分流、模式切换、模式根变量覆盖、 具体目录变量最高优先级、相对路径锚定 repo root、源码树配置项不随迁移等 6 个用例。 ## 仓库清理 - 删除旧版子智能体目录 sub_agent/(逻辑已统一在 easyagent/)。 - 删除误产生/垃圾文件:误敲命令生成的 "ystemctl status ..."、空文件 .zhouyanbo / testfile_from_ai、运行态日志/pid、test_playwright.png、test_system_message.py 等。 - 实验残留与历史文档(BUG_FIX/POLLING 变更日志、SUB_AGENT 文档、翻译资料、 model_tests、compact_result、奇奇怪怪的bug、截图、goal_research)归档到 本地 _experiments/,并从版本控制移除(已加入 .gitignore)。 - .gitignore:新增 api/、_experiments/,清理已失效的 sub_agent/* 条目与冗余项。 ## 文档 - CLAUDE.md / AGENTS.md:补充数据目录与路径变量、日志策略、迁移流程, 更新目录结构(移除 sub_agent、新增 _experiments/scripts),调试日志路径改为 ~/.agents/<mode>/logs/。
This commit is contained in:
parent
be08026fca
commit
eaa5e3bee9
15
.gitignore
vendored
15
.gitignore
vendored
@ -12,17 +12,17 @@ __pycache__/
|
|||||||
venv/
|
venv/
|
||||||
|
|
||||||
# Runtime data (main agent)
|
# Runtime data (main agent)
|
||||||
|
# 注意:运行态数据默认已迁出源码树到 ~/.agents/<mode>/。
|
||||||
|
# 以下条目用于忽略「通过具体目录变量指回源码树」或历史遗留的运行态目录。
|
||||||
logs/
|
logs/
|
||||||
data/
|
data/
|
||||||
project/
|
project/
|
||||||
users/
|
users/
|
||||||
|
api/
|
||||||
webapp.pid
|
webapp.pid
|
||||||
|
|
||||||
# Runtime data (sub agent)
|
# Runtime data (sub agent)
|
||||||
sub_agent/tasks/
|
# 旧版 sub_agent/ 子项目已移除,子智能体逻辑现统一在 easyagent/。
|
||||||
sub_agent/data/
|
|
||||||
sub_agent/logs/
|
|
||||||
sub_agent/project/
|
|
||||||
sub_agent_results/
|
sub_agent_results/
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
@ -33,10 +33,11 @@ static/dist/
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
user_upload/
|
user_upload/
|
||||||
skills/
|
skills/
|
||||||
ax_probe_chrome.py
|
|
||||||
|
|
||||||
# Ignore docs and runtime pid
|
# 本地实验残留与历史文档归档(不进仓库)
|
||||||
webapp.pid
|
_experiments/
|
||||||
|
|
||||||
|
# Ignore docs
|
||||||
doc/
|
doc/
|
||||||
|
|
||||||
# Host workspace config (local, use .example)
|
# Host workspace config (local, use .example)
|
||||||
|
|||||||
43
AGENTS.md
43
AGENTS.md
@ -31,7 +31,45 @@
|
|||||||
- `docs/cli_slash_commands_spec.md`: CLI `/` 指令设计说明
|
- `docs/cli_slash_commands_spec.md`: CLI `/` 指令设计说明
|
||||||
- **其他子项目/资源**
|
- **其他子项目/资源**
|
||||||
- `android-webview-app/`: Android WebView 客户端工程
|
- `android-webview-app/`: Android WebView 客户端工程
|
||||||
- `easyagent/`: 独立前端子目录(与根目录前端并存)
|
- `easyagent/`: 子智能体执行逻辑所在目录(当前所有子智能体执行均走这里;旧版 `sub_agent/` 已删除)
|
||||||
|
- `_experiments/`: 本地实验残留与历史文档归档目录(**不纳入 git**,见 §7)
|
||||||
|
|
||||||
|
## 1.5) 运行态数据目录与路径变量(2026-06 更新)
|
||||||
|
|
||||||
|
> 设计目标:运行态数据(对话、用户工作区、日志等)默认存放在用户主目录的 `~/.agents` 下,对齐 `~/.claude`、`~/.codex` 惯例,**不落在源码树内**。配置实现见 `config/paths.py`。
|
||||||
|
|
||||||
|
### 1.5.1 默认位置与模式分流
|
||||||
|
|
||||||
|
运行态根目录按运行模式自动分流(由 `TERMINAL_SANDBOX_MODE` 决定):
|
||||||
|
|
||||||
|
- 宿主机模式(`TERMINAL_SANDBOX_MODE=host`)→ `~/.agents/host`
|
||||||
|
- 其它模式(默认 docker / web)→ `~/.agents/web`
|
||||||
|
|
||||||
|
每个模式根下包含:`data/`(对话、用户库、记忆、sub_agents.json、sub_agent_tasks 等)、`users/`(web 多用户工作区)、`api/users/`(API 用户工作区)、`logs/`(日志)。
|
||||||
|
|
||||||
|
### 1.5.2 路径解析优先级(从高到低)
|
||||||
|
|
||||||
|
1. **具体目录环境变量**:`DATA_DIR` / `LOGS_DIR` / `USER_SPACE_DIR` / `API_USER_SPACE_DIR`(单独覆盖某个目录,最高优先级)
|
||||||
|
2. **模式根目录环境变量**:`AGENTS_HOST_HOME` 或 `AGENTS_WEB_HOME`(整体搬迁该模式下全部运行态数据)
|
||||||
|
3. **兜底默认**:`~/.agents/<mode>`
|
||||||
|
|
||||||
|
具体目录变量支持相对路径(相对仓库根目录展开)、绝对路径与 `~`。
|
||||||
|
|
||||||
|
> 注意:`config/*.json`(`custom_models.json`、`host_workspaces.json`、`auto_approval.json` 等)属于**配置而非运行态数据**,仍锚定源码树,不随运行态根目录迁移。`prompts/`、`agentskills/` 同理。
|
||||||
|
|
||||||
|
### 1.5.3 日志策略(2026-06)
|
||||||
|
|
||||||
|
- API 请求体落盘(dump)默认**关闭**,由 `AGENT_API_DUMP_ENABLED` 控制(`1/true/yes/on` 开启)。
|
||||||
|
- 日志混合轮转(实现见 `utils/log_rotation.py`):
|
||||||
|
- 追加型单文件(`host_workspace_debug.log`、`chunk_*.log`、`conn_diag.log`、`api_debug.log`、TaskLogger/ErrorLogger)按大小轮转,默认单文件 20MB、保留 3 份。
|
||||||
|
- 按份计量目录(`api_requests/` 一请求一文件)只保留最近 N 个,默认 30。
|
||||||
|
- 阈值可覆盖:`AGENT_LOG_ROTATE_MAX_BYTES` / `AGENT_LOG_ROTATE_BACKUPS` / `AGENT_DUMP_KEEP`。
|
||||||
|
|
||||||
|
### 1.5.4 数据迁移
|
||||||
|
|
||||||
|
- 迁移脚本:`scripts/migrate_runtime_data.py`(源码树 → 运行态根;复制+备份+可回滚+幂等,`logs/` 丢弃不迁)。
|
||||||
|
- 脚本 `import config` 复用程序同一套路径解析(含 `.env`),**模式由 `.env` 决定**;执行前务必先 `--dry-run` 确认目标。
|
||||||
|
- 清理误迁副本:`scripts/cleanup_misplaced_web.sh`。
|
||||||
|
|
||||||
## 2) 当前可用启动/构建命令(已按代码核对)
|
## 2) 当前可用启动/构建命令(已按代码核对)
|
||||||
|
|
||||||
@ -163,7 +201,8 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
|||||||
## 7) 安全与仓库卫生
|
## 7) 安全与仓库卫生
|
||||||
|
|
||||||
- 严禁提交真实密钥(`.env`、token、cookie、用户隐私)。
|
- 严禁提交真实密钥(`.env`、token、cookie、用户隐私)。
|
||||||
- `logs/`, `data/`, `users/`, `project/` 为运行态目录(见 `.gitignore`),分享前需脱敏。
|
- 运行态数据默认在 `~/.agents/<mode>/`(`data/`、`users/`、`logs/`、`api/`),不在源码树内;详见 §1.5。`.gitignore` 仍忽略源码树内的 `logs/`、`data/`、`users/`、`api/`、`project/` 等,以防通过具体变量指回源码树或历史遗留产生污染。分享前需脱敏。
|
||||||
|
- **`_experiments/` 用途**:归档本地实验残留与历史文档(调试记录、旧变更日志、模型测试脚本、翻译资料、旧子智能体文档等)。该目录**不纳入 git**(已在 `.gitignore`)。需要保留但不属于当前主线、又不想直接删的零散文件,统一放这里,不要散落在根目录。
|
||||||
- 不要把本地构建产物(如 `static/dist/`、`node_modules/`)纳入提交。
|
- 不要把本地构建产物(如 `static/dist/`、`node_modules/`)纳入提交。
|
||||||
|
|
||||||
## 8) Android App 发布联动要求(重要)
|
## 8) Android App 发布联动要求(重要)
|
||||||
|
|||||||
@ -1,107 +0,0 @@
|
|||||||
# Bug 修复说明
|
|
||||||
|
|
||||||
## 修复的问题
|
|
||||||
|
|
||||||
### 1. 内容显示两遍
|
|
||||||
**原因**: WebSocket 和轮询同时工作,导致事件被处理了两次
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 在 `app/state.ts` 中添加 `usePollingMode: true` 标志
|
|
||||||
- 在所有关键的 WebSocket 事件处理器开头添加检查:
|
|
||||||
```javascript
|
|
||||||
if (ctx.usePollingMode) {
|
|
||||||
return; // 跳过 WebSocket 事件
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 修改的事件包括:
|
|
||||||
- `ai_message_start`
|
|
||||||
- `thinking_start/chunk/end`
|
|
||||||
- `text_start/chunk/end`
|
|
||||||
- `tool_preparing/intent/status/start`
|
|
||||||
|
|
||||||
### 2. 刷新后内容消失
|
|
||||||
**原因**: `restoreTaskState()` 清空了 `messages` 数组,导致已加载的历史记录丢失
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 修改 `app/methods/taskPolling.ts` 中的 `restoreTaskState()` 方法
|
|
||||||
- 不再清空 `messages` 数组
|
|
||||||
- 只标记状态为进行中(`streamingMessage = true`, `taskInProgress = true`)
|
|
||||||
- 轮询只处理新事件,不重建历史记录
|
|
||||||
|
|
||||||
## 修改的文件
|
|
||||||
|
|
||||||
1. **static/src/app/state.ts**
|
|
||||||
- 添加 `usePollingMode: true` 标志
|
|
||||||
|
|
||||||
2. **static/src/app/methods/taskPolling.ts**
|
|
||||||
- 修改 `restoreTaskState()` 方法,不清空消息
|
|
||||||
|
|
||||||
3. **static/src/composables/useLegacySocket.ts**
|
|
||||||
- 在所有关键事件处理器开头添加轮询模式检查
|
|
||||||
- 修改的事件:
|
|
||||||
- `ai_message_start`
|
|
||||||
- `thinking_start`
|
|
||||||
- `thinking_chunk`
|
|
||||||
- `thinking_end`
|
|
||||||
- `text_start`
|
|
||||||
- `text_chunk`
|
|
||||||
- `text_end`
|
|
||||||
- `tool_preparing`
|
|
||||||
- `tool_intent`
|
|
||||||
- `tool_status`
|
|
||||||
- `tool_start`
|
|
||||||
|
|
||||||
## 工作流程
|
|
||||||
|
|
||||||
### 正常发送消息
|
|
||||||
1. 用户输入消息并发送
|
|
||||||
2. 调用 REST API 创建任务
|
|
||||||
3. 后端在独立线程中运行任务
|
|
||||||
4. 前端每秒轮询一次,获取新事件
|
|
||||||
5. 通过 `handleTaskEvent()` 处理事件,更新界面
|
|
||||||
6. WebSocket 事件被 `usePollingMode` 标志跳过
|
|
||||||
|
|
||||||
### 页面刷新恢复
|
|
||||||
1. 页面加载完成
|
|
||||||
2. 延迟 1 秒后调用 `restoreTaskState()`
|
|
||||||
3. 查找运行中的任务
|
|
||||||
4. 如果找到,标记状态为进行中
|
|
||||||
5. 启动轮询,只处理新事件
|
|
||||||
6. 已加载的历史记录保持不变
|
|
||||||
7. 新事件追加到现有消息后面
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
1. **基本功能**
|
|
||||||
- 发送消息,观察是否只显示一次
|
|
||||||
- 刷新页面,观察历史记录是否保留
|
|
||||||
- 观察新内容是否正常追加
|
|
||||||
|
|
||||||
2. **边界情况**
|
|
||||||
- 任务执行中多次刷新
|
|
||||||
- 任务完成后刷新
|
|
||||||
- 快速连续发送多条消息
|
|
||||||
|
|
||||||
3. **性能**
|
|
||||||
- 长时间运行任务
|
|
||||||
- 大量事件(观察是否有重复)
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd static
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
构建成功!
|
|
||||||
- `static/dist/assets/main.js` (687.87 kB)
|
|
||||||
- `static/dist/assets/task.js` (3.68 kB)
|
|
||||||
|
|
||||||
## 下一步
|
|
||||||
|
|
||||||
现在可以启动服务器测试:
|
|
||||||
```bash
|
|
||||||
python web_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
访问 http://localhost:8091 测试功能。
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
# Bug 修复 v2 - 工具块和轮询优化
|
|
||||||
|
|
||||||
## 修复的问题
|
|
||||||
|
|
||||||
### 1. 工具块无法显示
|
|
||||||
**原因**: 工具事件处理逻辑不完整,缺少关键的工具创建和更新逻辑
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 完整实现 `handleToolPreparing()` - 创建工具准备块
|
|
||||||
- 完整实现 `handleToolStart()` - 工具开始执行
|
|
||||||
- 完整实现 `handleToolUpdateAction()` - 工具状态更新和完成
|
|
||||||
- 添加 `update_action` 事件处理(工具完成的关键事件)
|
|
||||||
- 在 WebSocket 中跳过 `update_action` 事件
|
|
||||||
|
|
||||||
### 2. 轮询频率太慢(1秒)
|
|
||||||
**原因**: 1秒的轮询间隔无法提供流式输出的体验
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 将轮询间隔从 1000ms 改为 **150ms**
|
|
||||||
- 接近流式输出的效果(每秒约 6-7 次更新)
|
|
||||||
- 在 `stores/task.ts` 中修改 `startPolling()` 方法
|
|
||||||
|
|
||||||
### 3. 刷新后加载两遍内容
|
|
||||||
**原因**:
|
|
||||||
- `restoreTaskState()` 没有检查是否已在流式输出中
|
|
||||||
- `handleTaskComplete()` 会重新加载历史记录
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 在 `restoreTaskState()` 中添加状态检查,避免重复恢复
|
|
||||||
- 检查最后一条消息是否是 assistant 消息
|
|
||||||
- 在 `handleTaskComplete()` 中移除 `fetchAndDisplayHistory()`,只更新统计
|
|
||||||
|
|
||||||
## 修改的文件
|
|
||||||
|
|
||||||
### 1. static/src/app/methods/taskPolling.ts
|
|
||||||
**工具处理逻辑**:
|
|
||||||
```javascript
|
|
||||||
handleToolPreparing(data) {
|
|
||||||
// 创建工具准备块
|
|
||||||
const action = {
|
|
||||||
id: data.id,
|
|
||||||
type: 'tool',
|
|
||||||
tool: {
|
|
||||||
status: 'preparing',
|
|
||||||
name: data.name,
|
|
||||||
// ... 完整的工具属性
|
|
||||||
}
|
|
||||||
};
|
|
||||||
msg.actions.push(action);
|
|
||||||
this.preparingTools.set(data.id, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToolStart(data) {
|
|
||||||
// 从 preparing 转为 running
|
|
||||||
let action = this.preparingTools.get(data.preparing_id);
|
|
||||||
action.tool.status = 'running';
|
|
||||||
action.tool.arguments = data.arguments;
|
|
||||||
// ... 更新工具状态
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToolUpdateAction(data) {
|
|
||||||
// 更新工具状态(包括完成)
|
|
||||||
let targetAction = this.toolFindAction(data.id, ...);
|
|
||||||
targetAction.tool.status = data.status;
|
|
||||||
targetAction.tool.result = data.result;
|
|
||||||
// ... 更新工具结果
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**恢复逻辑优化**:
|
|
||||||
```javascript
|
|
||||||
async restoreTaskState() {
|
|
||||||
// 检查是否已在流式输出中
|
|
||||||
if (this.streamingMessage || this.taskInProgress) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已有 assistant 消息
|
|
||||||
const lastMessage = this.messages[this.messages.length - 1];
|
|
||||||
const hasAssistantMessage = lastMessage && lastMessage.role === 'assistant';
|
|
||||||
|
|
||||||
// 只标记状态,不清空消息
|
|
||||||
this.streamingMessage = true;
|
|
||||||
this.taskInProgress = true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**任务完成优化**:
|
|
||||||
```javascript
|
|
||||||
handleTaskComplete(data) {
|
|
||||||
// 只更新统计,不重新加载历史
|
|
||||||
this.fetchConversationTokenStatistics();
|
|
||||||
this.updateCurrentContextTokens();
|
|
||||||
// 移除了 fetchAndDisplayHistory()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. static/src/stores/task.ts
|
|
||||||
**轮询频率优化**:
|
|
||||||
```javascript
|
|
||||||
startPolling(eventHandler) {
|
|
||||||
// 150ms 间隔,接近流式输出效果
|
|
||||||
this.pollingInterval = window.setInterval(() => {
|
|
||||||
this.pollTaskEvents(handler);
|
|
||||||
}, 150);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. static/src/composables/useLegacySocket.ts
|
|
||||||
**跳过 update_action 事件**:
|
|
||||||
```javascript
|
|
||||||
ctx.socket.on('update_action', (data) => {
|
|
||||||
if (ctx.usePollingMode) {
|
|
||||||
return; // 跳过 WebSocket 事件
|
|
||||||
}
|
|
||||||
// ... 原有逻辑
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 事件处理流程
|
|
||||||
|
|
||||||
### 工具执行流程
|
|
||||||
1. `tool_preparing` → 创建工具准备块
|
|
||||||
2. `tool_start` → 工具开始执行,更新状态为 running
|
|
||||||
3. `update_action` → 工具完成,更新状态为 completed,显示结果
|
|
||||||
|
|
||||||
### 轮询流程
|
|
||||||
1. 每 150ms 轮询一次
|
|
||||||
2. 获取新事件(通过 `from` 参数)
|
|
||||||
3. 处理事件,更新界面
|
|
||||||
4. 任务完成后停止轮询
|
|
||||||
|
|
||||||
### 刷新恢复流程
|
|
||||||
1. 页面加载完成
|
|
||||||
2. 延迟 1 秒后调用 `restoreTaskState()`
|
|
||||||
3. 检查是否已在流式输出中(避免重复)
|
|
||||||
4. 检查是否已有 assistant 消息(历史已加载)
|
|
||||||
5. 只标记状态,启动轮询
|
|
||||||
6. 轮询只处理新事件,不重建历史
|
|
||||||
|
|
||||||
## 性能优化
|
|
||||||
|
|
||||||
- **轮询频率**: 150ms(每秒约 6-7 次)
|
|
||||||
- **事件增量获取**: 通过 `from` 参数只获取新事件
|
|
||||||
- **避免重复加载**: 检查状态和消息,避免重复恢复
|
|
||||||
- **避免重复显示**: 任务完成后不重新加载历史
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
1. **工具块显示**
|
|
||||||
- 发送需要调用工具的消息
|
|
||||||
- 观察工具块是否正常显示
|
|
||||||
- 观察工具状态变化(preparing → running → completed)
|
|
||||||
- 观察工具结果是否正确显示
|
|
||||||
|
|
||||||
2. **流式输出效果**
|
|
||||||
- 发送消息,观察输出是否流畅
|
|
||||||
- 观察思考块、文本块的更新频率
|
|
||||||
- 对比之前 1 秒轮询的卡顿感
|
|
||||||
|
|
||||||
3. **刷新恢复**
|
|
||||||
- 任务执行中刷新页面
|
|
||||||
- 观察是否只显示一遍内容
|
|
||||||
- 观察新内容是否正常追加
|
|
||||||
- 观察工具块是否保留
|
|
||||||
|
|
||||||
4. **任务完成**
|
|
||||||
- 等待任务完成
|
|
||||||
- 观察是否有重复内容
|
|
||||||
- 观察统计是否正确更新
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd static
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
构建成功!
|
|
||||||
- `static/dist/assets/main.js` (689.89 kB)
|
|
||||||
- `static/dist/assets/task.js` (3.68 kB)
|
|
||||||
|
|
||||||
## 已知改进
|
|
||||||
|
|
||||||
- ✅ 工具块正常显示
|
|
||||||
- ✅ 轮询频率提升到 150ms(接近流式效果)
|
|
||||||
- ✅ 刷新后不重复显示内容
|
|
||||||
- ✅ 任务完成后不重复加载历史
|
|
||||||
- ✅ 状态检查避免重复恢复
|
|
||||||
|
|
||||||
现在可以启动服务器测试了!
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
# Bug 修复 v3 - 刷新后重复加载问题
|
|
||||||
|
|
||||||
## 修复的问题
|
|
||||||
|
|
||||||
### 刷新后加载两遍内容
|
|
||||||
**原因**:
|
|
||||||
1. `loadInitialData()` 会调用 `fetchAndDisplayHistory()` 加载历史记录
|
|
||||||
2. 1 秒后 `restoreTaskState()` 启动轮询
|
|
||||||
3. `loadRunningTask()` 设置 `lastEventIndex = 0`,从头开始获取所有事件
|
|
||||||
4. 导致已经在历史中的事件被重复处理
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
|
|
||||||
#### 1. 等待历史加载完成
|
|
||||||
在 `restoreTaskState()` 中检查历史是否已加载:
|
|
||||||
```javascript
|
|
||||||
async restoreTaskState() {
|
|
||||||
// 检查历史是否已加载
|
|
||||||
const hasMessages = Array.isArray(this.messages) && this.messages.length > 0;
|
|
||||||
|
|
||||||
if (!hasMessages) {
|
|
||||||
// 等待历史加载完成后再恢复
|
|
||||||
setTimeout(() => {
|
|
||||||
this.restoreTaskState();
|
|
||||||
}, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 历史已加载,启动轮询
|
|
||||||
taskStore.startPolling(...);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 计算正确的事件偏移量
|
|
||||||
在 `loadRunningTask()` 中获取任务详情,计算已处理的事件数量:
|
|
||||||
```javascript
|
|
||||||
async loadRunningTask(conversationId) {
|
|
||||||
// 查找运行中的任务
|
|
||||||
const runningTask = result.data.find(...);
|
|
||||||
|
|
||||||
if (runningTask) {
|
|
||||||
// 获取任务详情,计算已处理的事件数量
|
|
||||||
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
|
|
||||||
const detailResult = await detailResponse.json();
|
|
||||||
|
|
||||||
// 设置为当前事件数量,只获取新事件
|
|
||||||
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length;
|
|
||||||
|
|
||||||
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 工作流程
|
|
||||||
|
|
||||||
### 页面刷新恢复流程(修复后)
|
|
||||||
|
|
||||||
1. **页面加载** (`mounted()`)
|
|
||||||
- 调用 `loadInitialData()`
|
|
||||||
- 加载历史记录(`fetchAndDisplayHistory()`)
|
|
||||||
- 1 秒后调用 `restoreTaskState()`
|
|
||||||
|
|
||||||
2. **任务恢复** (`restoreTaskState()`)
|
|
||||||
- 检查是否已在流式输出中 → 跳过
|
|
||||||
- 查找运行中的任务
|
|
||||||
- **检查历史是否已加载** → 如果未加载,等待 500ms 后重试
|
|
||||||
- 历史已加载 → 继续
|
|
||||||
|
|
||||||
3. **加载任务详情** (`loadRunningTask()`)
|
|
||||||
- 查找运行中的任务
|
|
||||||
- **获取任务详情**
|
|
||||||
- **计算已处理的事件数量**
|
|
||||||
- 设置 `lastEventIndex` 为当前事件数量
|
|
||||||
|
|
||||||
4. **启动轮询** (`startPolling()`)
|
|
||||||
- 从 `lastEventIndex` 开始轮询
|
|
||||||
- **只获取新事件**
|
|
||||||
- 每 150ms 轮询一次
|
|
||||||
|
|
||||||
5. **处理新事件**
|
|
||||||
- 只处理新产生的事件
|
|
||||||
- 追加到已有的历史记录后面
|
|
||||||
- 不重复显示已有内容
|
|
||||||
|
|
||||||
## 关键改进
|
|
||||||
|
|
||||||
### 1. 等待历史加载
|
|
||||||
- 在 `restoreTaskState()` 中检查 `messages` 是否已加载
|
|
||||||
- 如果未加载,延迟 500ms 后重试
|
|
||||||
- 确保历史加载完成后再启动轮询
|
|
||||||
|
|
||||||
### 2. 正确的事件偏移量
|
|
||||||
- 在 `loadRunningTask()` 中获取任务详情
|
|
||||||
- 计算已处理的事件数量(`next_offset` 或 `events.length`)
|
|
||||||
- 设置 `lastEventIndex` 为当前事件数量
|
|
||||||
- 轮询只获取新事件(`from=lastEventIndex`)
|
|
||||||
|
|
||||||
### 3. 避免重复处理
|
|
||||||
- 历史记录由 `fetchAndDisplayHistory()` 加载
|
|
||||||
- 轮询只处理新事件
|
|
||||||
- 不会重复显示已有内容
|
|
||||||
|
|
||||||
## 修改的文件
|
|
||||||
|
|
||||||
### 1. static/src/app/methods/taskPolling.ts
|
|
||||||
**等待历史加载**:
|
|
||||||
```javascript
|
|
||||||
async restoreTaskState() {
|
|
||||||
// 检查历史是否已加载
|
|
||||||
const hasMessages = Array.isArray(this.messages) && this.messages.length > 0;
|
|
||||||
|
|
||||||
if (!hasMessages) {
|
|
||||||
debugLog('[TaskPolling] 历史未加载,等待历史加载完成');
|
|
||||||
setTimeout(() => {
|
|
||||||
this.restoreTaskState();
|
|
||||||
}, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('[TaskPolling] 历史已加载,启动轮询');
|
|
||||||
// ... 启动轮询
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. static/src/stores/task.ts
|
|
||||||
**计算事件偏移量**:
|
|
||||||
```javascript
|
|
||||||
async loadRunningTask(conversationId) {
|
|
||||||
const runningTask = result.data.find(...);
|
|
||||||
|
|
||||||
if (runningTask) {
|
|
||||||
// 获取任务详情
|
|
||||||
const detailResponse = await fetch(`/api/tasks/${runningTask.task_id}`);
|
|
||||||
const detailResult = await detailResponse.json();
|
|
||||||
|
|
||||||
// 设置为当前事件数量
|
|
||||||
this.lastEventIndex = detailResult.data.next_offset || detailResult.data.events.length;
|
|
||||||
|
|
||||||
debugLog('[Task] 设置起始偏移量:', this.lastEventIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试场景
|
|
||||||
|
|
||||||
### 1. 正常发送消息
|
|
||||||
- 发送消息
|
|
||||||
- 观察输出是否正常
|
|
||||||
- 观察是否只显示一次
|
|
||||||
|
|
||||||
### 2. 刷新页面(任务进行中)
|
|
||||||
- 任务执行中刷新页面
|
|
||||||
- 观察历史记录是否正确显示
|
|
||||||
- 观察是否只显示一次
|
|
||||||
- 观察新内容是否正常追加
|
|
||||||
- 观察工具块是否保留
|
|
||||||
|
|
||||||
### 3. 刷新页面(任务完成后)
|
|
||||||
- 任务完成后刷新页面
|
|
||||||
- 观察历史记录是否完整
|
|
||||||
- 观察是否没有重复内容
|
|
||||||
|
|
||||||
### 4. 多次刷新
|
|
||||||
- 任务执行中多次刷新
|
|
||||||
- 观察每次刷新是否正常
|
|
||||||
- 观察是否有累积的重复内容
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd static
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
构建成功!
|
|
||||||
- `static/dist/assets/main.js` (690.04 kB)
|
|
||||||
- `static/dist/assets/task.js` (4.01 kB)
|
|
||||||
|
|
||||||
## 最终效果
|
|
||||||
|
|
||||||
- ✅ 刷新后不重复显示内容
|
|
||||||
- ✅ 历史记录正确加载
|
|
||||||
- ✅ 新事件正常追加
|
|
||||||
- ✅ 工具块正确显示
|
|
||||||
- ✅ 流畅的输出效果(150ms 轮询)
|
|
||||||
|
|
||||||
现在可以测试了!刷新页面应该只显示一次内容,新内容会正常追加。
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
# Bug 修复 v4 - 刷新后后端停止问题
|
|
||||||
|
|
||||||
## 修复的问题
|
|
||||||
|
|
||||||
### 刷新后后端直接停止,不继续执行
|
|
||||||
**原因**:
|
|
||||||
1. 页面刷新时 WebSocket 断开
|
|
||||||
2. `handle_disconnect` 检测到断开且没有其他连接
|
|
||||||
3. 设置 `stop_flags[f"user:{username}"]` 为 `True`
|
|
||||||
4. 任务检查停止标志时,会查找 `stop_flags[task_id]` 和 `stop_flags[f"user:{username}"]`
|
|
||||||
5. 发现用户级别的停止标志,任务停止执行
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
在 `handle_disconnect` 中检查是否有通过 REST API 创建的运行中任务。如果有,说明使用轮询模式,不应该停止任务。
|
|
||||||
|
|
||||||
## 修改的文件
|
|
||||||
|
|
||||||
### server/socket_handlers.py
|
|
||||||
|
|
||||||
**修改前**:
|
|
||||||
```python
|
|
||||||
@socketio.on('disconnect')
|
|
||||||
def handle_disconnect():
|
|
||||||
username = connection_users.pop(request.sid, None)
|
|
||||||
has_other_connection = False
|
|
||||||
if username:
|
|
||||||
for sid, user in connection_users.items():
|
|
||||||
if user == username:
|
|
||||||
has_other_connection = True
|
|
||||||
break
|
|
||||||
|
|
||||||
task_info = get_stop_flag(request.sid, username)
|
|
||||||
if isinstance(task_info, dict) and not has_other_connection:
|
|
||||||
task_info['stop'] = True # 设置停止标志
|
|
||||||
# ... 取消任务
|
|
||||||
|
|
||||||
clear_stop_flag(request.sid, None) # 清理所有停止标志
|
|
||||||
```
|
|
||||||
|
|
||||||
**修改后**:
|
|
||||||
```python
|
|
||||||
@socketio.on('disconnect')
|
|
||||||
def handle_disconnect():
|
|
||||||
username = connection_users.pop(request.sid, None)
|
|
||||||
has_other_connection = False
|
|
||||||
if username:
|
|
||||||
for sid, user in connection_users.items():
|
|
||||||
if user == username:
|
|
||||||
has_other_connection = True
|
|
||||||
break
|
|
||||||
|
|
||||||
# 检查是否有通过 REST API 创建的运行中任务
|
|
||||||
has_rest_api_task = False
|
|
||||||
if username and not has_other_connection:
|
|
||||||
try:
|
|
||||||
from .tasks import task_manager
|
|
||||||
running_tasks = [t for t in task_manager.list_tasks(username) if t.status == "running"]
|
|
||||||
if running_tasks:
|
|
||||||
has_rest_api_task = True
|
|
||||||
debug_log(f"[WebSocket] 用户 {username} 有运行中的 REST API 任务,不停止")
|
|
||||||
except Exception as e:
|
|
||||||
debug_log(f"[WebSocket] 检查 REST API 任务失败: {e}")
|
|
||||||
|
|
||||||
task_info = get_stop_flag(request.sid, username)
|
|
||||||
# 只有在没有其他连接且没有 REST API 任务时才停止
|
|
||||||
if isinstance(task_info, dict) and not has_other_connection and not has_rest_api_task:
|
|
||||||
task_info['stop'] = True
|
|
||||||
# ... 取消任务
|
|
||||||
|
|
||||||
# 清理停止标志(只清理 sid 级别的,不清理 user 级别的)
|
|
||||||
if request.sid in stop_flags:
|
|
||||||
stop_flags.pop(request.sid, None)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 工作流程
|
|
||||||
|
|
||||||
### 停止标志机制
|
|
||||||
|
|
||||||
**stop_flags 结构**:
|
|
||||||
```python
|
|
||||||
stop_flags = {
|
|
||||||
"client_sid": {"stop": bool, "task": asyncio.Task, "terminal": WebTerminal},
|
|
||||||
"user:{username}": {"stop": bool, "task": asyncio.Task, "terminal": WebTerminal},
|
|
||||||
"task_id": {"stop": bool, "task": None, "terminal": None}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**查找顺序**:
|
|
||||||
1. 先查找 `stop_flags[client_sid]`(WebSocket sid 或 task_id)
|
|
||||||
2. 再查找 `stop_flags[f"user:{username}"]`(用户级别)
|
|
||||||
|
|
||||||
### WebSocket 断开流程(修复后)
|
|
||||||
|
|
||||||
1. **检测断开**
|
|
||||||
- 移除 `connection_users[sid]`
|
|
||||||
- 检查是否有其他连接
|
|
||||||
|
|
||||||
2. **检查 REST API 任务**
|
|
||||||
- 查询 `task_manager.list_tasks(username)`
|
|
||||||
- 检查是否有 `status == "running"` 的任务
|
|
||||||
- 如果有,设置 `has_rest_api_task = True`
|
|
||||||
|
|
||||||
3. **决定是否停止**
|
|
||||||
- 如果有其他连接 → 不停止
|
|
||||||
- 如果有 REST API 任务 → 不停止
|
|
||||||
- 否则 → 停止任务
|
|
||||||
|
|
||||||
4. **清理停止标志**
|
|
||||||
- 只清理 `stop_flags[sid]`(sid 级别)
|
|
||||||
- 不清理 `stop_flags[f"user:{username}"]`(用户级别)
|
|
||||||
- 避免影响 REST API 任务
|
|
||||||
|
|
||||||
### 页面刷新流程(修复后)
|
|
||||||
|
|
||||||
1. **WebSocket 断开**
|
|
||||||
- 检测到有运行中的 REST API 任务
|
|
||||||
- 不设置停止标志
|
|
||||||
- 任务继续执行
|
|
||||||
|
|
||||||
2. **页面重新加载**
|
|
||||||
- 加载历史记录
|
|
||||||
- 恢复任务状态
|
|
||||||
- 启动轮询
|
|
||||||
|
|
||||||
3. **任务继续执行**
|
|
||||||
- 后端任务不受影响
|
|
||||||
- 继续生成事件
|
|
||||||
- 前端轮询获取新事件
|
|
||||||
|
|
||||||
## 关键改进
|
|
||||||
|
|
||||||
### 1. 检测 REST API 任务
|
|
||||||
- 在 WebSocket 断开时检查是否有运行中的 REST API 任务
|
|
||||||
- 如果有,说明使用轮询模式,不应该停止
|
|
||||||
|
|
||||||
### 2. 保护 REST API 任务
|
|
||||||
- 只有在没有其他连接且没有 REST API 任务时才停止
|
|
||||||
- 避免 WebSocket 断开影响 REST API 任务
|
|
||||||
|
|
||||||
### 3. 精确清理停止标志
|
|
||||||
- 只清理 sid 级别的停止标志
|
|
||||||
- 不清理用户级别的停止标志
|
|
||||||
- 避免误清理 REST API 任务的停止标志
|
|
||||||
|
|
||||||
## 测试场景
|
|
||||||
|
|
||||||
### 1. 正常发送消息
|
|
||||||
- 发送消息
|
|
||||||
- 观察任务是否正常执行
|
|
||||||
- 观察输出是否正常
|
|
||||||
|
|
||||||
### 2. 刷新页面(任务进行中)
|
|
||||||
- 任务执行中刷新页面
|
|
||||||
- **观察后端是否继续执行**
|
|
||||||
- 观察前端是否正常恢复
|
|
||||||
- 观察新内容是否正常追加
|
|
||||||
|
|
||||||
### 3. 多次刷新
|
|
||||||
- 任务执行中多次刷新
|
|
||||||
- 观察每次刷新后任务是否继续
|
|
||||||
- 观察是否有累积的问题
|
|
||||||
|
|
||||||
### 4. 停止任务
|
|
||||||
- 点击停止按钮
|
|
||||||
- 观察任务是否立即停止
|
|
||||||
- 观察前端状态是否正确
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd static
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
构建成功!
|
|
||||||
|
|
||||||
## 最终效果
|
|
||||||
|
|
||||||
- ✅ 刷新后后端继续执行
|
|
||||||
- ✅ 前端正常恢复状态
|
|
||||||
- ✅ 新内容正常追加
|
|
||||||
- ✅ 工具块正确显示
|
|
||||||
- ✅ 流畅的输出效果(150ms 轮询)
|
|
||||||
- ✅ 不重复显示内容
|
|
||||||
|
|
||||||
现在可以测试了!刷新页面后,后端应该继续执行,前端会正常恢复并显示新内容。
|
|
||||||
42
CLAUDE.md
42
CLAUDE.md
@ -121,11 +121,12 @@ npm --prefix cli run build
|
|||||||
# 查看容器状态
|
# 查看容器状态
|
||||||
docker ps -a | grep agent-term
|
docker ps -a | grep agent-term
|
||||||
|
|
||||||
# 查看调试日志
|
# 查看调试日志(运行态日志默认在 ~/.agents/<mode>/logs/)
|
||||||
tail -f logs/debug_stream.log
|
tail -f ~/.agents/host/logs/debug_stream.log # host 模式
|
||||||
|
tail -f ~/.agents/web/logs/debug_stream.log # web/docker 模式
|
||||||
|
|
||||||
# 查看容器统计
|
# 查看容器统计
|
||||||
tail -f logs/container_stats.log
|
tail -f ~/.agents/web/logs/container_stats.log
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@ -149,6 +150,24 @@ tail -f logs/container_stats.log
|
|||||||
- `PROJECT_MAX_STORAGE_MB` - 单用户磁盘配额 (默认 2048MB)
|
- `PROJECT_MAX_STORAGE_MB` - 单用户磁盘配额 (默认 2048MB)
|
||||||
- `MAX_ACTIVE_USER_CONTAINERS` - 并发容器数量 (默认 8)
|
- `MAX_ACTIVE_USER_CONTAINERS` - 并发容器数量 (默认 8)
|
||||||
|
|
||||||
|
**数据目录与路径变量** (`config/paths.py`,2026-06 更新):
|
||||||
|
|
||||||
|
运行态数据默认存放在 `~/.agents/<mode>`(对齐 `~/.claude`、`~/.codex`),**不落在源码树**。模式由 `TERMINAL_SANDBOX_MODE` 决定:`host` → `~/.agents/host`,其它(默认 docker/web)→ `~/.agents/web`。每个模式根下含 `data/`(对话、用户库、记忆、sub_agents.json、sub_agent_tasks)、`users/`、`api/users/`、`logs/`。
|
||||||
|
|
||||||
|
路径解析优先级(高→低):
|
||||||
|
1. 具体目录变量 `DATA_DIR` / `LOGS_DIR` / `USER_SPACE_DIR` / `API_USER_SPACE_DIR`(单独覆盖某目录)
|
||||||
|
2. 模式根变量 `AGENTS_HOST_HOME` / `AGENTS_WEB_HOME`(整体搬迁该模式数据)
|
||||||
|
3. 兜底 `~/.agents/<mode>`
|
||||||
|
|
||||||
|
> `config/*.json`、`prompts/`、`agentskills/` 属于配置/资源,仍锚定源码树,不随运行态迁移。
|
||||||
|
|
||||||
|
**日志策略**:
|
||||||
|
- API 请求体 dump 默认**关闭**,`AGENT_API_DUMP_ENABLED=1` 开启。
|
||||||
|
- 混合轮转(`utils/log_rotation.py`):追加型单文件按大小轮转(默认 20MB×3 份),dump 目录按份保留(默认 30 个)。阈值变量:`AGENT_LOG_ROTATE_MAX_BYTES` / `AGENT_LOG_ROTATE_BACKUPS` / `AGENT_DUMP_KEEP`。
|
||||||
|
|
||||||
|
**数据迁移**:
|
||||||
|
- `scripts/migrate_runtime_data.py`:源码树 → `~/.agents/<mode>`,复制+备份+可回滚+幂等,`logs/` 丢弃不迁。脚本复用 config 路径解析(模式由 `.env` 决定),执行前先 `--dry-run` 确认目标。
|
||||||
|
|
||||||
## Important Implementation Details
|
## Important Implementation Details
|
||||||
|
|
||||||
### File Operations
|
### File Operations
|
||||||
@ -281,20 +300,29 @@ agents/
|
|||||||
├── server/ # Flask 业务主线(chat/task/goal/monitor/auth ...)
|
├── server/ # Flask 业务主线(chat/task/goal/monitor/auth ...)
|
||||||
├── core/ # 核心终端与工具编排(main_terminal_parts/*)
|
├── core/ # 核心终端与工具编排(main_terminal_parts/*)
|
||||||
├── modules/ # 独立功能模块
|
├── modules/ # 独立功能模块
|
||||||
├── utils/ # 辅助工具
|
├── utils/ # 辅助工具(含 log_rotation 日志轮转)
|
||||||
├── config/ # 配置拆分(api/limits/terminal/paths ...,由 __init__ 聚合)
|
├── config/ # 配置拆分(api/limits/terminal/paths ...,由 __init__ 聚合)
|
||||||
├── static/ # Vue 3 + TS Web 前端
|
├── static/ # Vue 3 + TS Web 前端
|
||||||
├── cli/ # React + Ink CLI 前端(重写中)
|
├── cli/ # React + Ink CLI 前端(重写中)
|
||||||
|
├── easyagent/ # 子智能体执行逻辑(旧版 sub_agent/ 已删除)
|
||||||
├── docker/ # 容器镜像
|
├── docker/ # 容器镜像
|
||||||
├── prompts/ # 系统提示词
|
├── prompts/ # 系统提示词
|
||||||
|
├── scripts/ # 运维脚本(数据迁移 migrate_runtime_data.py 等)
|
||||||
├── android-webview-app/ # Android WebView 客户端工程
|
├── android-webview-app/ # Android WebView 客户端工程
|
||||||
├── test/ # 测试用例
|
├── test/ # 测试用例
|
||||||
├── docs/ / doc/ # 设计与发布文档
|
├── docs/ / doc/ # 设计与发布文档
|
||||||
├── users/ # 用户工作区(运行态,gitignored)
|
└── _experiments/ # 本地实验残留与历史文档归档(gitignored,不进仓库)
|
||||||
├── data/ # 全局数据(运行态,gitignored)
|
|
||||||
└── logs/ # 日志文件(运行态,gitignored)
|
运行态数据(不在源码树):
|
||||||
|
~/.agents/<mode>/ # mode = host 或 web,由 TERMINAL_SANDBOX_MODE 决定
|
||||||
|
├── data/ # 对话、用户库、记忆、sub_agent_tasks(运行态)
|
||||||
|
├── users/ # web 多用户工作区
|
||||||
|
├── api/users/ # API 用户工作区
|
||||||
|
└── logs/ # 日志文件
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 历史说明:`data/`、`users/`、`logs/`、`project/` 等过去曾在源码树内,现已默认迁至 `~/.agents/<mode>/`。详见 Configuration 节「数据目录与路径变量」。
|
||||||
|
|
||||||
## Development Tips
|
## Development Tips
|
||||||
|
|
||||||
- 修改系统提示词:编辑 `prompts/main_system.txt`
|
- 修改系统提示词:编辑 `prompts/main_system.txt`
|
||||||
|
|||||||
@ -1,180 +0,0 @@
|
|||||||
# 轮询模式改造完成
|
|
||||||
|
|
||||||
## 改造内容
|
|
||||||
|
|
||||||
已成功将前端从 WebSocket 实时推送模式改造为 REST API + 轮询模式。
|
|
||||||
|
|
||||||
### 主要变更
|
|
||||||
|
|
||||||
#### 1. 后端改造
|
|
||||||
- **server/tasks.py**: 已有完整的任务管理系统
|
|
||||||
- `POST /api/tasks` - 创建任务
|
|
||||||
- `GET /api/tasks/<task_id>?from=<offset>` - 轮询任务事件
|
|
||||||
- `POST /api/tasks/<task_id>/cancel` - 取消任务
|
|
||||||
- 支持 videos 和 run_mode 参数
|
|
||||||
|
|
||||||
#### 2. 前端改造
|
|
||||||
- **stores/task.ts** (新建): 任务轮询状态管理
|
|
||||||
- `createTask()` - 创建任务并启动轮询
|
|
||||||
- `pollTaskEvents()` - 轮询任务事件
|
|
||||||
- `startPolling()` / `stopPolling()` - 控制轮询
|
|
||||||
- `loadRunningTask()` - 加载运行中的任务(用于页面刷新恢复)
|
|
||||||
|
|
||||||
- **app/methods/taskPolling.ts** (新建): 事件处理器
|
|
||||||
- `handleTaskEvent()` - 统一事件分发
|
|
||||||
- `handleAiMessageStart()` - AI 消息开始
|
|
||||||
- `handleThinkingStart/Chunk/End()` - 思考过程
|
|
||||||
- `handleTextStart/Chunk/End()` - 文本输出
|
|
||||||
- `handleToolPreparing/Start/UpdateAction()` - 工具执行
|
|
||||||
- `handleAppendPayload/ModifyPayload()` - 文件操作
|
|
||||||
- `handleTaskComplete()` - 任务完成
|
|
||||||
- `restoreTaskState()` - 恢复任务状态(页面刷新后)
|
|
||||||
|
|
||||||
- **app/methods/message.ts**: 修改消息发送逻辑
|
|
||||||
- 改为调用 REST API 创建任务
|
|
||||||
- 停止任务改为调用 REST API 取消任务
|
|
||||||
- 保留 WebSocket 兼容性(命令模式)
|
|
||||||
|
|
||||||
- **app/lifecycle.ts**: 修改生命周期
|
|
||||||
- `mounted()` 中注册全局事件处理器
|
|
||||||
- `mounted()` 中延迟调用 `restoreTaskState()` 恢复任务
|
|
||||||
- `beforeUnmount()` 中停止轮询
|
|
||||||
|
|
||||||
- **app.ts**: 集成任务轮询方法
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
### ✅ 已实现
|
|
||||||
|
|
||||||
1. **后端独立运行**
|
|
||||||
- 任务创建后在后台线程运行
|
|
||||||
- 不依赖 WebSocket 连接
|
|
||||||
- 事件完整记录到内存队列(最多 1000 条)
|
|
||||||
|
|
||||||
2. **前端轮询更新**
|
|
||||||
- 1 秒间隔轮询任务事件
|
|
||||||
- 增量获取新事件(通过 `from` 参数)
|
|
||||||
- 任务完成后自动停止轮询
|
|
||||||
|
|
||||||
3. **页面刷新恢复**
|
|
||||||
- 页面加载时自动查找运行中的任务
|
|
||||||
- 从事件流重建前端状态
|
|
||||||
- 恢复思考块、文本块、工具块的展开状态
|
|
||||||
- 继续轮询更新新事件
|
|
||||||
|
|
||||||
4. **状态完整恢复**
|
|
||||||
- 正在输出的思考内容 → 展开的思考块 + 已输出内容
|
|
||||||
- 正在输出的文本内容 → 文本块 + 已输出内容
|
|
||||||
- 正在执行的工具 → 工具块 + 执行状态
|
|
||||||
- 文件操作 → append/modify payload 块
|
|
||||||
|
|
||||||
5. **任务取消**
|
|
||||||
- 支持通过 REST API 取消任务
|
|
||||||
- 前端立即清理状态
|
|
||||||
- 后端标记停止标志
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
|
|
||||||
### 正常使用
|
|
||||||
1. 输入消息并发送
|
|
||||||
2. 后端自动创建任务并开始执行
|
|
||||||
3. 前端每秒轮询一次,实时更新界面
|
|
||||||
4. 任务完成后自动停止轮询
|
|
||||||
|
|
||||||
### 页面刷新
|
|
||||||
1. 刷新页面
|
|
||||||
2. 前端自动检测运行中的任务
|
|
||||||
3. 从事件流重建状态(思考块、文本块、工具块)
|
|
||||||
4. 继续轮询更新新事件
|
|
||||||
5. 显示提示:"检测到进行中的任务,已恢复连接"
|
|
||||||
|
|
||||||
### 停止任务
|
|
||||||
1. 点击停止按钮
|
|
||||||
2. 调用 REST API 取消任务
|
|
||||||
3. 前端立即清理状态
|
|
||||||
4. 后端收到停止信号后终止执行
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### 轮询机制
|
|
||||||
- 间隔:1 秒
|
|
||||||
- 增量获取:通过 `from` 参数指定起始事件索引
|
|
||||||
- 自动停止:任务状态为 `succeeded/failed/canceled` 时停止
|
|
||||||
|
|
||||||
### 事件流格式
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"type": "ai_message_start",
|
|
||||||
"data": {},
|
|
||||||
"ts": 1234567890.123
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 任务状态
|
|
||||||
- `pending` - 待执行
|
|
||||||
- `running` - 执行中
|
|
||||||
- `succeeded` - 成功完成
|
|
||||||
- `failed` - 执行失败
|
|
||||||
- `canceled` - 已取消
|
|
||||||
|
|
||||||
### 事件类型
|
|
||||||
- `ai_message_start` - AI 消息开始
|
|
||||||
- `thinking_start/chunk/end` - 思考过程
|
|
||||||
- `text_start/chunk/end` - 文本输出
|
|
||||||
- `tool_preparing/start/update_action` - 工具执行
|
|
||||||
- `append_payload/modify_payload` - 文件操作
|
|
||||||
- `task_complete` - 任务完成
|
|
||||||
- `error` - 错误
|
|
||||||
- `token_update` - Token 统计更新
|
|
||||||
- `conversation_resolved` - 对话已解析
|
|
||||||
|
|
||||||
## 兼容性
|
|
||||||
|
|
||||||
- ✅ 保留 WebSocket 连接(用于命令模式和实时推送)
|
|
||||||
- ✅ 命令模式(`/clear` 等)仍使用 WebSocket
|
|
||||||
- ✅ 在线用户仍可收到实时推送(通过 `socketio.emit` 到房间)
|
|
||||||
- ✅ 支持多标签页同时查看进度
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
1. **基本功能测试**
|
|
||||||
- 发送消息,观察是否正常输出
|
|
||||||
- 刷新页面,观察是否恢复状态
|
|
||||||
- 停止任务,观察是否立即停止
|
|
||||||
|
|
||||||
2. **边界情况测试**
|
|
||||||
- 任务执行中刷新多次
|
|
||||||
- 任务完成后刷新
|
|
||||||
- 多个标签页同时打开
|
|
||||||
- 网络断开后重连
|
|
||||||
|
|
||||||
3. **性能测试**
|
|
||||||
- 长时间运行任务(观察轮询是否稳定)
|
|
||||||
- 大量事件(1000+ 条)
|
|
||||||
- 多用户并发
|
|
||||||
|
|
||||||
## 已知限制
|
|
||||||
|
|
||||||
1. **事件队列大小**: 最多保留 1000 条事件(deque maxlen=1000)
|
|
||||||
2. **任务持久化**: 当前仅内存存储,服务器重启后丢失
|
|
||||||
3. **并发限制**: 单用户单工作区同时只能有一个任务
|
|
||||||
|
|
||||||
## 后续优化建议
|
|
||||||
|
|
||||||
1. **持久化**: 将任务和事件存储到 Redis/数据库
|
|
||||||
2. **事件压缩**: 对历史事件进行压缩存储
|
|
||||||
3. **断点续传**: 支持从任意事件索引恢复
|
|
||||||
4. **WebSocket 降级**: 完全移除 WebSocket 依赖
|
|
||||||
5. **长轮询**: 使用 long-polling 减少请求次数
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd static
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
构建成功!输出文件:
|
|
||||||
- `static/dist/assets/task.js` (3.68 kB)
|
|
||||||
- `static/dist/assets/main.js` (687.51 kB)
|
|
||||||
Binary file not shown.
@ -1,211 +0,0 @@
|
|||||||
# 子智能体实现完成总结
|
|
||||||
|
|
||||||
## 已完成的工作
|
|
||||||
|
|
||||||
### 1. 复制 easyagent 代码 ✅
|
|
||||||
- 将 easyagent 的核心代码复制到 `easyagent/` 目录
|
|
||||||
- 包含 src/、prompts/、doc/、package.json
|
|
||||||
|
|
||||||
### 2. 创建批处理模式 ✅
|
|
||||||
- 创建 `easyagent/src/batch/index.js` 批处理入口
|
|
||||||
- 支持命令行参数:workspace、task-file、system-prompt-file、output-file、stats-file、agent-id、timeout
|
|
||||||
- 实现对话循环和工具执行
|
|
||||||
- 添加 finish_task 工具定义
|
|
||||||
- 实现兜底机制:自动提醒调用 finish_task
|
|
||||||
- 实现对话记录保存到 `.subagent/conversation.json`
|
|
||||||
- 实现超时和轮次限制保护
|
|
||||||
|
|
||||||
### 3. 更新工具定义 ✅
|
|
||||||
- 更新 `core/main_terminal_parts/tools_definition.py`
|
|
||||||
- 替换旧的 create_sub_agent 工具定义
|
|
||||||
- 移除 wait_sub_agent 工具(合并到 create_sub_agent 的 run_in_background 参数)
|
|
||||||
- 添加 terminate_sub_agent 工具
|
|
||||||
- 添加 get_sub_agent_status 工具
|
|
||||||
- 详细的工具描述,包括使用场景和限制
|
|
||||||
|
|
||||||
### 4. 更新工具执行 ✅
|
|
||||||
- 更新 `core/main_terminal_parts/tools_execution.py`
|
|
||||||
- 修改 create_sub_agent 执行逻辑:
|
|
||||||
- 支持 deliverables_dir 参数(替代 target_dir)
|
|
||||||
- 支持 run_in_background 参数
|
|
||||||
- 阻塞模式自动等待完成
|
|
||||||
- 添加 terminate_sub_agent 执行逻辑
|
|
||||||
- 添加 get_sub_agent_status 执行逻辑
|
|
||||||
|
|
||||||
### 5. 更新 SubAgentManager ✅
|
|
||||||
- 已有新的实现(子进程模式)
|
|
||||||
- 支持启动 Node.js 子进程
|
|
||||||
- 实现任务状态管理
|
|
||||||
- 实现进程监控和终止
|
|
||||||
- 更新 `_build_system_prompt` 为完整版本:
|
|
||||||
- 详细的工作流程
|
|
||||||
- 工作原则(专注性、独立性、效率性、完整性)
|
|
||||||
- 交付要求
|
|
||||||
- 工具使用说明
|
|
||||||
- 注意事项
|
|
||||||
- 当前环境信息
|
|
||||||
|
|
||||||
### 6. 创建文档 ✅
|
|
||||||
- `SUB_AGENT_IMPLEMENTATION_PLAN.md` - 完整实现方案
|
|
||||||
- `SUB_AGENT_TESTING.md` - 测试指南
|
|
||||||
|
|
||||||
## 核心特性
|
|
||||||
|
|
||||||
### 1. 简化的参数
|
|
||||||
- 去掉 reference_files(子智能体自己搜索文件)
|
|
||||||
- 去掉 delivery_mode(统一为文件交付)
|
|
||||||
- 去掉 reason 参数(终止时不需要原因)
|
|
||||||
- deliverables_dir 可选(默认 sub_agent_results/agent_{id})
|
|
||||||
|
|
||||||
### 2. 后台运行机制
|
|
||||||
- run_in_background 参数(默认 false)
|
|
||||||
- 后台模式:立即返回,完成后通知
|
|
||||||
- 阻塞模式:等待完成后返回结果
|
|
||||||
- 工具描述中明确说明何时使用
|
|
||||||
|
|
||||||
### 3. 完成任务工具
|
|
||||||
- 只需两个参数:success 和 summary
|
|
||||||
- 调用后立即退出
|
|
||||||
- 兜底机制:忘记调用时自动提醒
|
|
||||||
|
|
||||||
### 4. 统计信息
|
|
||||||
- 运行时间
|
|
||||||
- 文件读取次数
|
|
||||||
- 搜索次数
|
|
||||||
- 网页查看次数
|
|
||||||
- 命令执行次数
|
|
||||||
- Token 使用量
|
|
||||||
|
|
||||||
### 5. 对话记录
|
|
||||||
- 保存在交付目录的 `.subagent/conversation.json`
|
|
||||||
- 包含完整的消息历史
|
|
||||||
- 包含统计信息
|
|
||||||
- 便于调试和审计
|
|
||||||
|
|
||||||
### 6. 安全限制
|
|
||||||
- 最多 5 个并发子智能体
|
|
||||||
- 禁止操作相同文件/目录
|
|
||||||
- 禁止工作重叠
|
|
||||||
- 超时自动终止
|
|
||||||
- 轮次限制防止无限循环
|
|
||||||
|
|
||||||
## 待完成的工作
|
|
||||||
|
|
||||||
### 1. 后台任务轮询机制 ⏳
|
|
||||||
需要在主智能体的工具执行后添加检查逻辑:
|
|
||||||
```python
|
|
||||||
# 在每次工具执行后
|
|
||||||
completed_agents = sub_agent_manager.poll_updates()
|
|
||||||
if completed_agents:
|
|
||||||
# 插入 system 消息通知
|
|
||||||
for agent in completed_agents:
|
|
||||||
system_message = build_completion_message(agent)
|
|
||||||
messages.append({"role": "system", "content": system_message})
|
|
||||||
# 继续调用 API
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 对话状态管理 ⏳
|
|
||||||
需要在主智能体完成但子智能体未完成时:
|
|
||||||
- 设置对话状态标记
|
|
||||||
- 前端显示警告(切换对话会终止子智能体)
|
|
||||||
- 轮询等待完成
|
|
||||||
- 完成后发送 user 消息触发新一轮
|
|
||||||
|
|
||||||
### 3. 前端集成 ⏳
|
|
||||||
- 显示子智能体运行状态
|
|
||||||
- 显示进度和统计信息
|
|
||||||
- 支持手动终止
|
|
||||||
- 支持查看对话记录
|
|
||||||
|
|
||||||
### 4. 测试和调试 ⏳
|
|
||||||
- 按照 SUB_AGENT_TESTING.md 进行测试
|
|
||||||
- 修复发现的 bug
|
|
||||||
- 优化性能
|
|
||||||
|
|
||||||
### 5. 模型配置 ⏳
|
|
||||||
需要将 easyagent 的 models.json 配置集成到主项目:
|
|
||||||
- 复制或链接 models.json
|
|
||||||
- 或者从主项目的配置生成 models.json
|
|
||||||
|
|
||||||
## 文件清单
|
|
||||||
|
|
||||||
### 新增文件
|
|
||||||
- `easyagent/` - easyagent 代码目录
|
|
||||||
- `easyagent/src/batch/index.js` - 批处理入口
|
|
||||||
- `SUB_AGENT_IMPLEMENTATION_PLAN.md` - 实现方案
|
|
||||||
- `SUB_AGENT_TESTING.md` - 测试指南
|
|
||||||
- `SUB_AGENT_COMPLETION_SUMMARY.md` - 本文件
|
|
||||||
|
|
||||||
### 修改文件
|
|
||||||
- `core/main_terminal_parts/tools_definition.py` - 工具定义
|
|
||||||
- `core/main_terminal_parts/tools_execution.py` - 工具执行
|
|
||||||
- `modules/sub_agent_manager.py` - 子智能体管理器(已有新实现)
|
|
||||||
|
|
||||||
## 下一步行动
|
|
||||||
|
|
||||||
1. **测试批处理模式**
|
|
||||||
```bash
|
|
||||||
cd /tmp && mkdir test_subagent
|
|
||||||
# 按照 SUB_AGENT_TESTING.md 的步骤测试
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **配置模型**
|
|
||||||
```bash
|
|
||||||
cp ~/Desktop/easyagent/models.json /Users/jojo/Desktop/agents/正在修复中/agents/easyagent/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **测试主智能体集成**
|
|
||||||
- 启动 Web 服务器
|
|
||||||
- 创建子智能体
|
|
||||||
- 观察执行过程
|
|
||||||
|
|
||||||
4. **添加轮询机制**
|
|
||||||
- 在 WebTerminal 或主循环中添加 poll_updates 调用
|
|
||||||
- 实现 system 消息插入
|
|
||||||
- 实现 user 消息触发
|
|
||||||
|
|
||||||
5. **完善前端**
|
|
||||||
- 显示子智能体状态
|
|
||||||
- 添加控制按钮
|
|
||||||
- 优化用户体验
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **Node.js 依赖**
|
|
||||||
- 确保 Node.js 18+ 已安装
|
|
||||||
- 运行 `npm install` 安装依赖
|
|
||||||
|
|
||||||
2. **路径问题**
|
|
||||||
- easyagent 批处理文件的路径是硬编码的
|
|
||||||
- 如果项目位置改变,需要更新 SubAgentManager 中的路径
|
|
||||||
|
|
||||||
3. **模型配置**
|
|
||||||
- easyagent 需要 models.json 配置文件
|
|
||||||
- 需要与主项目的模型配置保持一致
|
|
||||||
|
|
||||||
4. **权限问题**
|
|
||||||
- 确保交付目录有写权限
|
|
||||||
- 确保 .subagent 目录可以创建
|
|
||||||
|
|
||||||
5. **调试**
|
|
||||||
- 查看 logs/debug_stream.log
|
|
||||||
- 查看子进程的 stdout/stderr
|
|
||||||
- 查看 output.json 和 stats.json
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
核心功能已经实现完成,包括:
|
|
||||||
- ✅ 批处理模式
|
|
||||||
- ✅ 工具定义和执行
|
|
||||||
- ✅ 子智能体管理
|
|
||||||
- ✅ 对话记录保存
|
|
||||||
- ✅ 统计信息收集
|
|
||||||
- ✅ 兜底机制
|
|
||||||
|
|
||||||
还需要完成:
|
|
||||||
- ⏳ 后台任务轮询
|
|
||||||
- ⏳ 对话状态管理
|
|
||||||
- ⏳ 前端集成
|
|
||||||
- ⏳ 测试和调试
|
|
||||||
|
|
||||||
整体架构清晰,代码结构合理,可以开始测试和完善了。
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
# 子智能体问题修复总结
|
|
||||||
|
|
||||||
## 已修复的问题
|
|
||||||
|
|
||||||
### 1. 前端子智能体显示问题 ✅
|
|
||||||
- **问题**:创建子智能体后不显示,只在完成后突然显示
|
|
||||||
- **原因**:`get_overview` 方法中调用了旧的 `_call_service`(HTTP 模式),但新实现是子进程模式
|
|
||||||
- **修复**:移除 `_call_service` 调用,改为直接检查进程状态
|
|
||||||
|
|
||||||
### 2. 暗色模式适配 ✅
|
|
||||||
- **问题**:子智能体卡片没有暗色模式样式
|
|
||||||
- **修复**:在 `static/src/styles/components/panels/_left-panel.scss` 中添加暗色模式样式
|
|
||||||
|
|
||||||
### 3. deliverables_dir 参数问题 ✅
|
|
||||||
- **问题**:模型每次调用时都忘了提供这个参数
|
|
||||||
- **修复**:将 `deliverables_dir` 改为必需参数(在 `required` 数组中)
|
|
||||||
|
|
||||||
### 4. 后台运行时的 system 消息插入 ✅
|
|
||||||
- **状态**:已经实现
|
|
||||||
- **位置**:`server/chat_flow_task_support.py` 中的 `process_sub_agent_updates` 函数
|
|
||||||
- **调用**:在 `server/chat_flow_tool_loop.py` 的 `execute_tool_calls` 中每次工具执行后调用
|
|
||||||
|
|
||||||
## 还需要实现的功能
|
|
||||||
|
|
||||||
### 空闲期间的 user 消息自动发送 ⏳
|
|
||||||
|
|
||||||
**需求**:
|
|
||||||
- 当主智能体完成任务进入空闲状态时
|
|
||||||
- 如果还有子智能体在后台运行
|
|
||||||
- 需要轮询等待子智能体完成
|
|
||||||
- 完成后自动发送 user 消息触发新一轮对话
|
|
||||||
|
|
||||||
**实现位置**:
|
|
||||||
需要在主对话循环结束时添加检查逻辑,可能的位置:
|
|
||||||
1. `server/chat_flow_stream_loop.py` - 流式循环结束时
|
|
||||||
2. `server/chat_flow_task_main.py` - 主任务完成时
|
|
||||||
|
|
||||||
**实现逻辑**:
|
|
||||||
```python
|
|
||||||
# 在对话循环结束时
|
|
||||||
async def check_background_sub_agents(web_terminal, messages, sender):
|
|
||||||
manager = getattr(web_terminal, "sub_agent_manager", None)
|
|
||||||
if not manager:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查是否有运行中的子智能体
|
|
||||||
running_tasks = [
|
|
||||||
task for task in manager.tasks.values()
|
|
||||||
if task.get("status") not in {"completed", "failed", "timeout", "terminated"}
|
|
||||||
and task.get("run_in_background")
|
|
||||||
]
|
|
||||||
|
|
||||||
if not running_tasks:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 设置对话状态:有子智能体运行中
|
|
||||||
# 前端显示警告:切换对话会终止子智能体
|
|
||||||
|
|
||||||
# 轮询等待完成
|
|
||||||
while running_tasks:
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
updates = manager.poll_updates()
|
|
||||||
|
|
||||||
for update in updates:
|
|
||||||
# 发送 user 消息触发新一轮
|
|
||||||
user_message = build_completion_user_message(update)
|
|
||||||
messages.append({
|
|
||||||
"role": "user",
|
|
||||||
"content": user_message,
|
|
||||||
"metadata": {"sub_agent_completion": True}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 触发新一轮 API 调用
|
|
||||||
sender('user_message', {'content': user_message})
|
|
||||||
return True # 表示需要继续对话
|
|
||||||
|
|
||||||
# 重新检查运行中的任务
|
|
||||||
running_tasks = [
|
|
||||||
task for task in manager.tasks.values()
|
|
||||||
if task.get("status") not in {"completed", "failed", "timeout", "terminated"}
|
|
||||||
and task.get("run_in_background")
|
|
||||||
]
|
|
||||||
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
**user 消息格式**:
|
|
||||||
```python
|
|
||||||
def build_completion_user_message(update):
|
|
||||||
agent_id = update.get("agent_id")
|
|
||||||
summary = update.get("summary")
|
|
||||||
result_summary = update.get("result_summary")
|
|
||||||
deliverables_dir = update.get("deliverables_dir")
|
|
||||||
|
|
||||||
return f"""子智能体{agent_id} ({summary}) 已完成任务。
|
|
||||||
|
|
||||||
{result_summary}
|
|
||||||
|
|
||||||
交付目录:{deliverables_dir}"""
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试清单
|
|
||||||
|
|
||||||
- [x] 创建子智能体后立即在前端显示
|
|
||||||
- [x] 暗色模式下子智能体卡片样式正确
|
|
||||||
- [x] deliverables_dir 必须提供,否则报错
|
|
||||||
- [x] 后台运行时工具执行后插入 system 消息
|
|
||||||
- [ ] 空闲期间子智能体完成后自动发送 user 消息
|
|
||||||
- [ ] 切换对话时提示会终止子智能体
|
|
||||||
- [ ] 强制切换对话时终止所有子智能体
|
|
||||||
|
|
||||||
## 下一步
|
|
||||||
|
|
||||||
1. 实现空闲期间的 user 消息自动发送
|
|
||||||
2. 添加对话状态管理(标记有子智能体运行中)
|
|
||||||
3. 前端添加警告提示(切换对话会终止子智能体)
|
|
||||||
4. 测试完整流程
|
|
||||||
@ -1,382 +0,0 @@
|
|||||||
# 子智能体实现方案
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
将 easyagent (Node.js) 集成为主智能体的子智能体系统,支持后台并行执行独立任务。
|
|
||||||
|
|
||||||
## 核心设计
|
|
||||||
|
|
||||||
### 1. 工具定义
|
|
||||||
|
|
||||||
#### create_sub_agent - 创建子智能体
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
- `agent_id` (int, 必需): 子智能体编号 1-99,同一对话中唯一
|
|
||||||
- `summary` (str, 必需): 任务摘要 10-30 字
|
|
||||||
- `task` (str, 必需): 详细任务描述,包括目标、要求、交付内容、工作范围
|
|
||||||
- `deliverables_dir` (str, 可选): 交付目录相对路径,默认 `sub_agent_results/agent_{agent_id}`
|
|
||||||
- `run_in_background` (bool, 默认 false): 是否后台运行
|
|
||||||
- `timeout_seconds` (int, 默认 600): 超时时间 60-3600 秒
|
|
||||||
|
|
||||||
**何时使用后台运行:**
|
|
||||||
- 任务耗时较长(预计超过 5 分钟)
|
|
||||||
- 可以继续处理其他工作,不需要立即使用结果
|
|
||||||
- 多个独立任务可以并行执行
|
|
||||||
|
|
||||||
**何时使用阻塞运行(默认):**
|
|
||||||
- 任务较快(几分钟内完成)
|
|
||||||
- 后续工作依赖子智能体的结果
|
|
||||||
- 需要立即查看和使用输出
|
|
||||||
|
|
||||||
**返回格式(阻塞模式):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"agent_id": 1,
|
|
||||||
"status": "completed",
|
|
||||||
"summary": "生成API文档",
|
|
||||||
"message": "子智能体1 已完成任务",
|
|
||||||
"deliverables_dir": "docs/api",
|
|
||||||
"deliverables_files": ["api.md", "endpoints.json"],
|
|
||||||
"result_summary": "已生成 API 文档,包含 15 个端点的详细说明。",
|
|
||||||
"sub_conversation_id": "conv_abc123",
|
|
||||||
"stats": {
|
|
||||||
"runtime_seconds": 120,
|
|
||||||
"files_read": 8,
|
|
||||||
"searches": 3,
|
|
||||||
"web_pages": 0,
|
|
||||||
"commands": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**返回格式(后台模式):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"agent_id": 1,
|
|
||||||
"status": "running",
|
|
||||||
"summary": "生成API文档",
|
|
||||||
"message": "子智能体1 已启动,正在后台运行",
|
|
||||||
"deliverables_dir": "docs/api",
|
|
||||||
"sub_conversation_id": "conv_abc123",
|
|
||||||
"background": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### terminate_sub_agent - 终止子智能体
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
- `agent_id` (int, 必需): 要终止的子智能体编号
|
|
||||||
|
|
||||||
**返回格式:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"agent_id": 1,
|
|
||||||
"status": "terminated",
|
|
||||||
"message": "子智能体1 已被终止",
|
|
||||||
"partial_results": true,
|
|
||||||
"deliverables_dir": "docs/api"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### get_sub_agent_status - 查询子智能体状态
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
- `agent_ids` (list[int], 必需): 要查询的子智能体编号列表
|
|
||||||
|
|
||||||
**返回格式:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"agents": [
|
|
||||||
{
|
|
||||||
"agent_id": 1,
|
|
||||||
"summary": "生成API文档",
|
|
||||||
"status": "running",
|
|
||||||
"runtime_seconds": 45,
|
|
||||||
"files_read": 5,
|
|
||||||
"searches": 2,
|
|
||||||
"web_pages": 0,
|
|
||||||
"commands": 1,
|
|
||||||
"last_action": "正在读取 src/api/routes.py",
|
|
||||||
"deliverables_dir": "docs/api"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### finish_task - 完成任务(子智能体工具)
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
- `success` (bool, 必需): 任务是否成功完成
|
|
||||||
- `summary` (str, 必需): 完成摘要 50-200 字,说明完成了什么、生成了哪些文件、关键发现
|
|
||||||
|
|
||||||
**返回格式:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "任务已完成,子智能体即将退出",
|
|
||||||
"will_terminate": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 执行流程
|
|
||||||
|
|
||||||
#### 阻塞模式(run_in_background=false)
|
|
||||||
|
|
||||||
```
|
|
||||||
主智能体调用 create_sub_agent
|
|
||||||
↓
|
|
||||||
启动 Node.js 子进程(easyagent 批处理模式)
|
|
||||||
↓
|
|
||||||
子进程读取任务文件和系统提示
|
|
||||||
↓
|
|
||||||
子进程执行对话循环
|
|
||||||
↓
|
|
||||||
子进程调用 finish_task 或超时
|
|
||||||
↓
|
|
||||||
主智能体读取输出文件
|
|
||||||
↓
|
|
||||||
返回结果给主智能体
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 后台模式(run_in_background=true)
|
|
||||||
|
|
||||||
```
|
|
||||||
主智能体调用 create_sub_agent
|
|
||||||
↓
|
|
||||||
启动 Node.js 子进程(easyagent 批处理模式)
|
|
||||||
↓
|
|
||||||
立即返回 "running" 状态
|
|
||||||
↓
|
|
||||||
主智能体继续工作
|
|
||||||
↓
|
|
||||||
每次工具执行后,检查子智能体是否完成
|
|
||||||
↓
|
|
||||||
如果完成,插入 system 消息通知
|
|
||||||
↓
|
|
||||||
继续调用 API
|
|
||||||
↓
|
|
||||||
如果主智能体完成但子智能体未完成,设置对话状态
|
|
||||||
↓
|
|
||||||
轮询等待子智能体完成
|
|
||||||
↓
|
|
||||||
完成后发送 user 消息触发新一轮
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 消息拼接
|
|
||||||
|
|
||||||
#### 初始 user 消息(发送给子智能体)
|
|
||||||
|
|
||||||
```
|
|
||||||
你是子智能体 {agent_id},负责以下任务:
|
|
||||||
|
|
||||||
【任务摘要】
|
|
||||||
{summary}
|
|
||||||
|
|
||||||
【任务详情】
|
|
||||||
{task}
|
|
||||||
|
|
||||||
【交付目录】
|
|
||||||
{deliverables_dir}
|
|
||||||
|
|
||||||
【要求】
|
|
||||||
1. 完成任务后,将所有结果文件放到交付目录
|
|
||||||
2. 使用 finish_task 工具提交任务完成报告
|
|
||||||
3. 任务超时时间:{timeout_seconds} 秒
|
|
||||||
|
|
||||||
现在开始执行任务。
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 完成通知(system 消息,工具执行后)
|
|
||||||
|
|
||||||
```
|
|
||||||
子智能体{agent_id} ({summary}) 已完成。
|
|
||||||
|
|
||||||
运行时间:{runtime_seconds}秒
|
|
||||||
阅读了 {files_read} 个文件
|
|
||||||
搜索了 {searches} 次
|
|
||||||
查看了 {web_pages} 个网页
|
|
||||||
运行了 {commands} 个指令
|
|
||||||
|
|
||||||
交付目录:{deliverables_dir}
|
|
||||||
结果摘要:{result_summary}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 完成通知(user 消息,主智能体空闲时)
|
|
||||||
|
|
||||||
```
|
|
||||||
子智能体{agent_id} ({summary}) 已完成任务。
|
|
||||||
|
|
||||||
{result_summary}
|
|
||||||
|
|
||||||
交付目录:{deliverables_dir}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 兜底机制
|
|
||||||
|
|
||||||
如果子智能体输出结束但未调用 finish_task,自动发送 user 消息:
|
|
||||||
|
|
||||||
```
|
|
||||||
如果你已经完成了任务,请调用 finish_task 工具提交完成报告。如果还没有完成,请继续执行任务。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 子智能体 System Prompt
|
|
||||||
|
|
||||||
```
|
|
||||||
你是一个专注的子智能体,负责独立完成分配的任务。
|
|
||||||
|
|
||||||
# 身份定位
|
|
||||||
|
|
||||||
你是主智能体创建的子智能体,拥有完整的工具能力(读写文件、执行命令、搜索网页等)。你的职责是专注完成分配的单一任务,不要偏离任务目标。
|
|
||||||
|
|
||||||
# 工作流程
|
|
||||||
|
|
||||||
1. **理解任务**:仔细阅读任务描述,明确目标和要求
|
|
||||||
2. **制定计划**:规划完成任务的步骤
|
|
||||||
3. **执行任务**:使用工具完成各个步骤
|
|
||||||
4. **生成交付**:将所有结果文件放到指定的交付目录
|
|
||||||
5. **提交报告**:使用 finish_task 工具提交完成报告并退出
|
|
||||||
|
|
||||||
# 工作原则
|
|
||||||
|
|
||||||
## 专注性
|
|
||||||
- 只完成分配的任务,不要做额外的工作
|
|
||||||
- 不要尝试与用户对话或询问问题
|
|
||||||
- 遇到问题时,在能力范围内解决或在报告中说明
|
|
||||||
|
|
||||||
## 独立性
|
|
||||||
- 你与主智能体共享工作区,可以访问所有文件
|
|
||||||
- 你的工作范围应该与其他子智能体不重叠
|
|
||||||
- 不要修改任务描述之外的文件
|
|
||||||
|
|
||||||
## 效率性
|
|
||||||
- 直接开始工作,不要过度解释
|
|
||||||
- 合理使用工具,避免重复操作
|
|
||||||
- 注意超时限制,在时间内完成核心工作
|
|
||||||
|
|
||||||
## 完整性
|
|
||||||
- 确保交付目录中的文件完整可用
|
|
||||||
- 生成的文档要清晰、格式正确
|
|
||||||
- 代码要包含必要的注释和说明
|
|
||||||
|
|
||||||
# 交付要求
|
|
||||||
|
|
||||||
所有结果文件必须放在指定的交付目录中,包括:
|
|
||||||
- 主要成果文件(文档、代码、报告等)
|
|
||||||
- 支持文件(数据、配置、示例等)
|
|
||||||
- 不要在交付目录外创建文件
|
|
||||||
|
|
||||||
# 完成任务
|
|
||||||
|
|
||||||
任务完成后,必须调用 finish_task 工具:
|
|
||||||
- success: 是否成功完成
|
|
||||||
- summary: 完成摘要(说明做了什么、生成了什么)
|
|
||||||
|
|
||||||
调用 finish_task 后,你会立即退出,无法继续工作。
|
|
||||||
|
|
||||||
# 工具使用
|
|
||||||
|
|
||||||
你拥有以下工具能力:
|
|
||||||
- read_file: 读取文件内容
|
|
||||||
- write_file / edit_file: 创建或修改文件
|
|
||||||
- search_workspace: 搜索文件和代码
|
|
||||||
- run_command: 执行终端命令
|
|
||||||
- web_search / extract_webpage: 搜索和提取网页内容
|
|
||||||
- finish_task: 完成任务并退出(必须调用)
|
|
||||||
|
|
||||||
# 注意事项
|
|
||||||
|
|
||||||
1. **不要无限循环**:如果任务无法完成,说明原因并提交报告
|
|
||||||
2. **不要超出范围**:只操作任务描述中指定的文件/目录
|
|
||||||
3. **不要等待输入**:你是自主运行的,不会收到用户的进一步指令
|
|
||||||
4. **注意时间限制**:超时会被强制终止,优先完成核心工作
|
|
||||||
|
|
||||||
# 当前环境
|
|
||||||
|
|
||||||
- 工作区路径: {workspace}
|
|
||||||
- 系统: {system}
|
|
||||||
- 当前时间: {current_time}
|
|
||||||
|
|
||||||
现在开始执行任务。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 对话记录存储
|
|
||||||
|
|
||||||
子智能体的对话记录保存在:`{deliverables_dir}/.subagent/conversations.json`
|
|
||||||
|
|
||||||
格式:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"agent_id": 1,
|
|
||||||
"summary": "生成API文档",
|
|
||||||
"created_at": "2026-03-10T12:00:00Z",
|
|
||||||
"completed_at": "2026-03-10T12:02:00Z",
|
|
||||||
"messages": [
|
|
||||||
{"role": "system", "content": "..."},
|
|
||||||
{"role": "user", "content": "..."},
|
|
||||||
{"role": "assistant", "content": "..."},
|
|
||||||
...
|
|
||||||
],
|
|
||||||
"stats": {
|
|
||||||
"runtime_seconds": 120,
|
|
||||||
"files_read": 8,
|
|
||||||
"searches": 3,
|
|
||||||
"web_pages": 0,
|
|
||||||
"commands": 2,
|
|
||||||
"token_usage": {"prompt": 1000, "completion": 500, "total": 1500}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
project/
|
|
||||||
├── sub_agent_results/
|
|
||||||
│ └── agent_1/
|
|
||||||
│ ├── .subagent/
|
|
||||||
│ │ └── conversations.json # 对话记录
|
|
||||||
│ ├── api.md # 交付文件
|
|
||||||
│ ├── endpoints.json
|
|
||||||
│ └── ...
|
|
||||||
├── docs/
|
|
||||||
│ └── api/
|
|
||||||
│ ├── .subagent/
|
|
||||||
│ │ └── conversations.json
|
|
||||||
│ ├── api.md
|
|
||||||
│ └── ...
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. 限制和约束
|
|
||||||
|
|
||||||
1. **最多 5 个并发子智能体**
|
|
||||||
2. **禁止多个子智能体操作相同文件或目录**
|
|
||||||
3. **禁止子智能体间的工作重叠**
|
|
||||||
4. **超时后强制终止,已生成的部分结果保留**
|
|
||||||
5. **切换对话会强制终止所有子智能体**
|
|
||||||
|
|
||||||
### 9. 实现步骤
|
|
||||||
|
|
||||||
1. ✅ 复制 easyagent 代码到 `easyagent/` 目录
|
|
||||||
2. ✅ 创建 `easyagent/src/batch/index.js` 批处理入口
|
|
||||||
3. ⏳ 修改 `SubAgentManager` 使用新的批处理模式
|
|
||||||
4. ⏳ 添加工具定义到 `core/tool_config.py`
|
|
||||||
5. ⏳ 修改主智能体的工具执行逻辑
|
|
||||||
6. ⏳ 添加后台任务轮询机制
|
|
||||||
7. ⏳ 添加对话状态管理(子智能体运行中)
|
|
||||||
8. ⏳ 测试和调试
|
|
||||||
|
|
||||||
## 关键改进点
|
|
||||||
|
|
||||||
1. **简化参数**:去掉 reference_files、delivery_mode、reason 等不必要参数
|
|
||||||
2. **清晰的后台机制**:默认阻塞,明确说明何时用后台
|
|
||||||
3. **详细的统计信息**:工具使用次数一目了然
|
|
||||||
4. **智能通知**:system 消息(工具执行后)+ user 消息(主智能体空闲时)
|
|
||||||
5. **防止冲突**:工具描述中明确禁止重叠工作
|
|
||||||
6. **兜底机制**:自动提醒调用 finish_task
|
|
||||||
7. **对话记录保留**:便于调试和审计
|
|
||||||
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
# 子智能体集成测试
|
|
||||||
|
|
||||||
本文档说明如何测试新的子智能体系统。
|
|
||||||
|
|
||||||
## 前置条件
|
|
||||||
|
|
||||||
1. 确保 Node.js 已安装(18+)
|
|
||||||
2. 安装 easyagent 依赖:
|
|
||||||
```bash
|
|
||||||
cd easyagent
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 确保 models.json 配置正确(复制 easyagent 根目录的 models.json 到 easyagent/ 目录)
|
|
||||||
|
|
||||||
## 测试步骤
|
|
||||||
|
|
||||||
### 1. 测试批处理模式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建测试目录
|
|
||||||
mkdir -p /tmp/test_subagent
|
|
||||||
cd /tmp/test_subagent
|
|
||||||
|
|
||||||
# 创建测试任务文件
|
|
||||||
cat > task.txt << 'EOF'
|
|
||||||
你是子智能体 1,负责以下任务:
|
|
||||||
|
|
||||||
**任务摘要**:创建测试文件
|
|
||||||
|
|
||||||
**任务详情**:
|
|
||||||
在当前目录创建一个名为 test.txt 的文件,内容为 "Hello from sub-agent"。
|
|
||||||
|
|
||||||
**交付目录**:/tmp/test_subagent/deliverables
|
|
||||||
请将所有生成的文件保存到此目录。
|
|
||||||
|
|
||||||
**超时时间**:60 秒
|
|
||||||
|
|
||||||
完成任务后,请调用 finish_task 工具提交完成报告。
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 创建系统提示文件
|
|
||||||
cat > system.txt << 'EOF'
|
|
||||||
你是一个专注的子智能体,负责独立完成分配的任务。
|
|
||||||
完成任务后,必须调用 finish_task 工具。
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 运行批处理
|
|
||||||
node /Users/jojo/Desktop/agents/正在修复中/agents/easyagent/src/batch/index.js \
|
|
||||||
--workspace /tmp/test_subagent \
|
|
||||||
--task-file task.txt \
|
|
||||||
--system-prompt-file system.txt \
|
|
||||||
--output-file output.json \
|
|
||||||
--stats-file stats.json \
|
|
||||||
--agent-id 1 \
|
|
||||||
--timeout 60
|
|
||||||
|
|
||||||
# 检查结果
|
|
||||||
cat output.json
|
|
||||||
ls -la deliverables/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 测试主智能体集成
|
|
||||||
|
|
||||||
启动 Web 服务器:
|
|
||||||
```bash
|
|
||||||
cd /Users/jojo/Desktop/agents/正在修复中/agents
|
|
||||||
python -m server.app
|
|
||||||
```
|
|
||||||
|
|
||||||
在浏览器中测试:
|
|
||||||
|
|
||||||
1. 登录系统
|
|
||||||
2. 创建新对话
|
|
||||||
3. 发送消息:
|
|
||||||
```
|
|
||||||
请创建一个子智能体,编号1,任务是在项目根目录创建一个 test.md 文件,内容为"测试子智能体功能"。
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 观察子智能体执行过程
|
|
||||||
5. 检查交付目录:`sub_agent_results/agent_1/`
|
|
||||||
|
|
||||||
### 3. 测试后台运行
|
|
||||||
|
|
||||||
发送消息:
|
|
||||||
```
|
|
||||||
请创建一个后台运行的子智能体,编号2,任务是搜索项目中所有的 Python 文件并统计行数,生成报告到 reports/line_count.md。设置为后台运行。
|
|
||||||
```
|
|
||||||
|
|
||||||
然后继续发送其他消息,观察子智能体完成后的通知。
|
|
||||||
|
|
||||||
### 4. 测试多个并行子智能体
|
|
||||||
|
|
||||||
发送消息:
|
|
||||||
```
|
|
||||||
请同时创建3个子智能体:
|
|
||||||
1. 编号1:分析 core/ 目录的代码结构
|
|
||||||
2. 编号2:分析 modules/ 目录的代码结构
|
|
||||||
3. 编号3:分析 utils/ 目录的代码结构
|
|
||||||
|
|
||||||
都设置为后台运行,交付目录分别为 reports/core、reports/modules、reports/utils。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 测试查询状态
|
|
||||||
|
|
||||||
在子智能体运行时,发送消息:
|
|
||||||
```
|
|
||||||
查询子智能体 1、2、3 的状态
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 测试终止子智能体
|
|
||||||
|
|
||||||
发送消息:
|
|
||||||
```
|
|
||||||
终止子智能体 2
|
|
||||||
```
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
1. **批处理模式**:
|
|
||||||
- output.json 包含 success 和 summary
|
|
||||||
- deliverables/ 目录包含生成的文件
|
|
||||||
- deliverables/.subagent/conversation.json 包含对话记录
|
|
||||||
|
|
||||||
2. **主智能体集成**:
|
|
||||||
- 阻塞模式:立即返回完成结果
|
|
||||||
- 后台模式:立即返回 running 状态,完成后收到通知
|
|
||||||
|
|
||||||
3. **并行执行**:
|
|
||||||
- 多个子智能体同时运行
|
|
||||||
- 各自独立完成任务
|
|
||||||
- 不会相互干扰
|
|
||||||
|
|
||||||
4. **状态查询**:
|
|
||||||
- 返回每个子智能体的运行时间、工具使用统计
|
|
||||||
- 显示当前状态(running/completed)
|
|
||||||
|
|
||||||
5. **终止功能**:
|
|
||||||
- 子智能体进程被终止
|
|
||||||
- 部分结果保留
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### 1. Node.js 找不到
|
|
||||||
|
|
||||||
确保 Node.js 已安装并在 PATH 中:
|
|
||||||
```bash
|
|
||||||
which node
|
|
||||||
node --version
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 模型配置错误
|
|
||||||
|
|
||||||
检查 easyagent/models.json 是否存在且配置正确。
|
|
||||||
|
|
||||||
### 3. 权限问题
|
|
||||||
|
|
||||||
确保交付目录有写权限:
|
|
||||||
```bash
|
|
||||||
chmod -R 755 sub_agent_results/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 子进程无法启动
|
|
||||||
|
|
||||||
检查 easyagent 批处理文件路径是否正确:
|
|
||||||
```bash
|
|
||||||
ls -la /Users/jojo/Desktop/agents/正在修复中/agents/easyagent/src/batch/index.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 对话记录未保存
|
|
||||||
|
|
||||||
检查交付目录的 .subagent/ 子目录权限。
|
|
||||||
|
|
||||||
## 调试技巧
|
|
||||||
|
|
||||||
1. 查看子进程输出:
|
|
||||||
```bash
|
|
||||||
tail -f logs/debug_stream.log
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 查看任务状态文件:
|
|
||||||
```bash
|
|
||||||
cat data/sub_agent_state.json
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 查看子智能体输出:
|
|
||||||
```bash
|
|
||||||
cat sub_agent_tasks/sub_1_*/output.json
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 查看统计信息:
|
|
||||||
```bash
|
|
||||||
cat sub_agent_tasks/sub_1_*/stats.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## 下一步
|
|
||||||
|
|
||||||
测试通过后,可以:
|
|
||||||
1. 添加更多工具到子智能体
|
|
||||||
2. 优化性能和错误处理
|
|
||||||
3. 添加前端展示界面
|
|
||||||
4. 完善文档和示例
|
|
||||||
@ -1,353 +0,0 @@
|
|||||||
# 对话已被第1次压缩
|
|
||||||
|
|
||||||
## 工作总结
|
|
||||||
## 📋 工作总结报告
|
|
||||||
|
|
||||||
### 1. 任务目标与用户真实诉求
|
|
||||||
|
|
||||||
用户要求修复手机端前端的三个核心问题:
|
|
||||||
- **问题1**:权限切换菜单在移动端跑出屏幕右侧,需自适应位置
|
|
||||||
- **问题2**:设置菜单中"用量统计"点击无法展开面板(移动端限制导致)
|
|
||||||
- **问题3**:审批面板在移动端自动关闭,需保持手动控制,且配色需与电脑端一致
|
|
||||||
|
|
||||||
额外需求:移除审批面板顶部灰色块,让内容顶头显示。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 已完成工作(按时间顺序)
|
|
||||||
|
|
||||||
| 时间 | 工作内容 | 状态 |
|
|
||||||
|------|---------|------|
|
|
||||||
| 初始 | 查看 Git 状态,分析未提交修改 | ✅ |
|
|
||||||
| 阶段1 | 分批提交 run_python 功能 | ✅ |
|
|
||||||
| 阶段2 | 修复权限菜单自适应位置(添加媒体查询) | ✅ |
|
|
||||||
| 阶段3 | 修复用量统计点击问题(添加 `fromSettingsMenu` 参数穿透) | ✅ |
|
|
||||||
| 阶段4 | 修复审批面板自动关闭问题(移除自动关闭逻辑) | ✅ |
|
|
||||||
| 阶段5 | 禁用电脑端审批面板拖拽展开 | ✅ |
|
|
||||||
| 阶段6 | 恢复审批面板点击背景关闭功能 | ✅ |
|
|
||||||
| 阶段7 | 尝试修复审批面板顶部灰色块(进行中) | ⚠️ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 关键决策与原因
|
|
||||||
|
|
||||||
**决策1:用量统计参数穿透**
|
|
||||||
- 原因:移动端禁用用量面板展开,但设置菜单应例外
|
|
||||||
- 方案:通过 `fromSettingsMenu` 参数绕过移动端限制
|
|
||||||
- 实现:修改事件链路上的三个文件(QuickMenu.vue → InputComposer.vue → App.vue)
|
|
||||||
|
|
||||||
**决策2:审批面板不再自动关闭**
|
|
||||||
- 原因:手机端与电脑端行为应一致,允许手动打开空面板
|
|
||||||
- 方案:移除 `fetchPendingToolApprovals` 和 `taskPolling` 中的自动关闭逻辑
|
|
||||||
|
|
||||||
**决策3:审批面板配色统一**
|
|
||||||
- 原因:手机端与电脑端视觉不一致
|
|
||||||
- 方案:添加 `@media (max-width: 768px)` 样式覆盖
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 修改过的文件与核心变更点
|
|
||||||
|
|
||||||
| 文件 | 核心变更 |
|
|
||||||
|------|---------|
|
|
||||||
| `static/src/styles/components/input/_composer.scss` | 权限菜单自适应、审批面板按钮样式、头部背景透明 |
|
|
||||||
| `static/src/app/methods/ui.ts` | `handleTokenPanelToggleClick` 添加参数、`fetchPendingToolApprovals` 移除自动关闭、`handleApprovalPanelToggleClick` 移动端逻辑 |
|
|
||||||
| `static/src/components/input/QuickMenu.vue` | 用量统计按钮事件添加 `true` 参数 |
|
|
||||||
| `static/src/components/input/InputComposer.vue` | 事件转发添加参数穿透 `(val) => $emit(..., val)` |
|
|
||||||
| `static/src/App.vue` | 审批面板遮罩层添加 `@click.self`、事件绑定添加参数 |
|
|
||||||
| `static/src/stores/resource.ts` | `toggleTokenPanel` 添加调试日志 |
|
|
||||||
| `static/src/composables/usePanelResize.ts` | 禁用右侧面板折叠时拖拽展开 |
|
|
||||||
| `static/src/app/methods/taskPolling.ts` | 移除审批完成后自动关闭面板逻辑 |
|
|
||||||
| `static/src/components/panels/ToolApprovalPanel.vue` | **进行中**:添加 `mobile-header` class 和样式 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 工具调用中的重要结果/错误与修复
|
|
||||||
|
|
||||||
**重要修复1:事件参数丢失**
|
|
||||||
- 错误:`fromSettingsMenu: false` 始终为 false
|
|
||||||
- 原因:Vue 事件转发未传递 `$event` 参数
|
|
||||||
- 修复:改为 `@toggle-token-panel="(val) => $emit('toggle-token-panel', val)"`
|
|
||||||
|
|
||||||
**重要修复2:审批面板瞬间关闭**
|
|
||||||
- 原因:`fetchPendingToolApprovals` 在空列表时自动关闭面板
|
|
||||||
- 修复:移除 `if (items.length === 0) closeMobileOverlay()` 逻辑
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. 当前未完成事项与下一步计划
|
|
||||||
|
|
||||||
**未完成事项:审批面板顶部灰色块**
|
|
||||||
|
|
||||||
已尝试方案:
|
|
||||||
1. ✅ 在 `_composer.scss` 中添加 `.mobile-panel-sheet--approval .sidebar-header` 样式
|
|
||||||
2. ✅ 使用 `!important` 提升优先级
|
|
||||||
3. ✅ 在 `ToolApprovalPanel.vue` 添加 `mobile-header` class 和条件渲染
|
|
||||||
|
|
||||||
**下一步计划:**
|
|
||||||
方案A:直接在 `ToolApprovalPanel.vue` 组件底部添加 `<style scoped>` 覆盖头部样式
|
|
||||||
```vue
|
|
||||||
<style scoped>
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.sidebar-header.mobile-header {
|
|
||||||
background: transparent !important;
|
|
||||||
padding-top: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
方案B:检查父组件是否有更高优先级的样式覆盖
|
|
||||||
|
|
||||||
**当前构建状态**:✅ 已构建完成(v2.97s),可立即测试
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. 风险与注意事项
|
|
||||||
|
|
||||||
1. **样式优先级**:移动端样式可能被其他高优先级规则覆盖,需持续检查
|
|
||||||
2. **事件链**:用量统计功能依赖多层事件转发,后续修改需注意参数传递
|
|
||||||
3. **调试日志**:当前代码包含 `[UI_DEBUG]` 日志,生产环境可清理
|
|
||||||
4. **浏览器缓存**:无痕模式仍有缓存,建议硬刷新(Ctrl+F5)测试
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 待执行操作
|
|
||||||
|
|
||||||
如需继续修复灰色块问题,请执行:
|
|
||||||
1. 确认 `ToolApprovalPanel.vue` 的 scoped style 是否正确添加
|
|
||||||
2. 重新构建:`npm run build`
|
|
||||||
3. 硬刷新浏览器测试
|
|
||||||
|
|
||||||
当前代码已保存,可直接继续修改。
|
|
||||||
|
|
||||||
## 用户的所有输入(仅文字)
|
|
||||||
1. 查看当前项目还有哪些没提交的修改
|
|
||||||
2. 看一下具体内容,看看能分成几次commit
|
|
||||||
3. 没问题,提交并push
|
|
||||||
4. 查看之前的提交记录,格式是 英文摘要+中文说明
|
|
||||||
5. 再做几个修改 手机版前端有一些问题 首先是权限切换的弹出选项菜单会有部分内容跑到屏幕右侧外,应该改为自适应左右位置,保证菜单完整显示
|
|
||||||
还有一个问题手机端之前的一次修改中,让上下文快捷查看的圆圈点击不会打开用量菜单,这个修改导致手机端点击设置菜单里的用量统计也不显示菜单里
|
|
||||||
最后一个问题,手机版的审批面板由于按钮位置和关闭面板的点击区域重合了,手机端的浏览器点击滞留效果总导致手机端一点击打开审批面板就自动关上了,要改为手机端的面板只能点击叉号关闭,手机端的审批面板的设计配色和电脑端也不太一样,改为一样的
|
|
||||||
修完不要提交,我要看看效果
|
|
||||||
6. build一下
|
|
||||||
7. 再修改一个地方,审批菜单的侧边栏不能通过拖拽屏幕边缘展开
|
|
||||||
8. 不是手机端的,是电脑端的
|
|
||||||
9. 手机版点击审批面板瞬间关闭和设置里的用量统计点击不会展开弹窗的bug依然没有修复,请你加入控制台调试日志,我复现一遍给你日志
|
|
||||||
10. 不要用debug标识,有太多的用这个了
|
|
||||||
11. 我复现了,点了三次审批面板,两次用量统计
|
|
||||||
[UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick called, isMobileViewport: true currentConversationId: conv_20260412_124008_293 activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick: mobile path, fetching approvals and opening overlay
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay called, target: approval isMobileViewport: true activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay: opening target: approval
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: approval
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick called, isMobileViewport: true currentConversationId: conv_20260412_124008_293 activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick: mobile path, fetching approvals and opening overlay
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay called, target: approval isMobileViewport: true activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay: opening target: approval
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: approval
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick called, isMobileViewport: true currentConversationId: conv_20260412_124008_293 activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick: mobile path, fetching approvals and opening overlay
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay called, target: approval isMobileViewport: true activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay: opening target: approval
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: approval
|
|
||||||
main.js:213 [UI_DEBUG] QuickMenu 用量统计按钮 clicked, emitting toggle-token-panel with true
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick called, fromSettingsMenu: false isMobileViewport: true tokenPanelCollapsed: true currentConversationId: conv_20260412_124008_293
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick: blocked - mobile viewport restriction
|
|
||||||
main.js:213 [UI_DEBUG] QuickMenu 用量统计按钮 clicked, emitting toggle-token-panel with true
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick called, fromSettingsMenu: false isMobileViewport: true tokenPanelCollapsed: true currentConversationId: conv_20260412_124008_293
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick: blocked - mobile viewport restriction
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: null
|
|
||||||
12. 你看看吧
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick called, isMobileViewport: true currentConversationId: conv_20260412_124450_725 activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick: mobile path, fetching approvals and opening overlay
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay called, target: approval isMobileViewport: true activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay: opening target: approval
|
|
||||||
main.js:214 [UI_DEBUG] fetchPendingToolApprovals: closing approval panel because items.length is 0
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: fetch-empty activeMobileOverlay: approval
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick called, isMobileViewport: true currentConversationId: conv_20260412_124450_725 activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] handleApprovalPanelToggleClick: mobile path, fetching approvals and opening overlay
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay called, target: approval isMobileViewport: true activeMobileOverlay: null
|
|
||||||
main.js:214 [UI_DEBUG] openMobileOverlay: opening target: approval
|
|
||||||
main.js:214 [UI_DEBUG] fetchPendingToolApprovals: closing approval panel because items.length is 0
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: fetch-empty activeMobileOverlay: approval
|
|
||||||
main.js:213 [UI_DEBUG] QuickMenu 用量统计按钮 clicked, emitting toggle-token-panel with true
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick called, fromSettingsMenu: false isMobileViewport: true tokenPanelCollapsed: true currentConversationId: conv_20260412_124450_725
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick: blocked - mobile viewport restriction
|
|
||||||
main.js:213 [UI_DEBUG] QuickMenu 用量统计按钮 clicked, emitting toggle-token-panel with true
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick called, fromSettingsMenu: false isMobileViewport: true tokenPanelCollapsed: true currentConversationId: conv_20260412_124450_725
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick: blocked - mobile viewport restriction
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: null
|
|
||||||
13. 还是不行
|
|
||||||
main.js:213 [UI_DEBUG] QuickMenu 用量统计按钮 clicked, emitting toggle-token-panel with true
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick called, fromSettingsMenu: false isMobileViewport: true tokenPanelCollapsed: true currentConversationId: conv_20260412_124450_725
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick: blocked - mobile viewport restriction
|
|
||||||
main.js:213 [UI_DEBUG] QuickMenu 用量统计按钮 clicked, emitting toggle-token-panel with true
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick called, fromSettingsMenu: false isMobileViewport: true tokenPanelCollapsed: true currentConversationId: conv_20260412_124450_725
|
|
||||||
main.js:214 [UI_DEBUG] handleTokenPanelToggleClick: blocked - mobile viewport restriction
|
|
||||||
main.js:214 [UI_DEBUG] closeMobileOverlay called from: unknown activeMobileOverlay: null
|
|
||||||
14. 好的,可以正常打开了,现在把手机端审批菜单的也修复一下吧,你看看电脑端的代码,电脑端是可以在没有待审批时手动打开的,看看是不是这里的逻辑不一样
|
|
||||||
15. 现在没问题了,把样式改一下,不要最上面这个空白的灰色的块,内容直接顶头,把点击空白区域关闭面板也加回来
|
|
||||||
16. 点击外部关闭好了,但顶部依然有灰色块
|
|
||||||
17. 依然有,我用的无痕模式都有,和之前一模一样
|
|
||||||
|
|
||||||
## 最近5条工具调用(参数 + 结果)
|
|
||||||
### 1. edit_file
|
|
||||||
- tool_call_id: functions.edit_file:166
|
|
||||||
- 参数:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"file_path": "static/src/components/panels/ToolApprovalPanel.vue",
|
|
||||||
"intent": "用条件class处理移动端样式",
|
|
||||||
"old_string": " <div class=\"sidebar-header\" style=\"@media (max-width: 768px) { background: transparent !important; padding-top: 44px; }\">\n <h3 class=\"icon-label\">工具审批 ({{ approvals.length }})</h3>\n <button type=\"button\" class=\"approval-close-btn\" aria-label=\"关闭审批面板\" @click=\"handleCloseClick\">\n ×\n </button>\n </div>",
|
|
||||||
"new_string": " <div class=\"sidebar-header\" :class=\"{ 'mobile-header': isMobileViewport }\">\n <h3 class=\"icon-label\">工具审批 ({{ approvals.length }})</h3>\n <button type=\"button\" class=\"approval-close-btn\" aria-label=\"关闭审批面板\" @click=\"handleCloseClick\">\n ×\n </button>\n </div>"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 结果:
|
|
||||||
```text
|
|
||||||
替换 1 处: static/src/components/panels/ToolApprovalPanel.vue
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. read_file
|
|
||||||
- tool_call_id: functions.read_file:167
|
|
||||||
- 参数:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"path": "static/src/components/panels/ToolApprovalPanel.vue",
|
|
||||||
"type": "extract",
|
|
||||||
"intent": "查看script部分",
|
|
||||||
"segments": [
|
|
||||||
{
|
|
||||||
"start_line": 115,
|
|
||||||
"end_line": 145
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 结果:
|
|
||||||
```text
|
|
||||||
从 static/src/components/panels/ToolApprovalPanel.vue 抽取 1 个片段 (max_chars=50000)
|
|
||||||
[segment_1] 行 115~145
|
|
||||||
```
|
|
||||||
collapsed: boolean;
|
|
||||||
width: number;
|
|
||||||
approvals: Array<any>;
|
|
||||||
decidingApprovalIds?: string[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: 'approve', approvalId: string): void;
|
|
||||||
(event: 'reject', approvalId: string): void;
|
|
||||||
(event: 'switch-unrestricted', approvalId: string): void;
|
|
||||||
(event: 'close'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const handleCloseClick = () => {
|
|
||||||
console.log('[UI_DEBUG] ToolApprovalPanel close button clicked');
|
|
||||||
emit('close');
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEditPreview = (item: any) => item?.tool_name === 'edit_file' && item?.preview?.edit_context;
|
|
||||||
const isCommandPreview = (item: any) =>
|
|
||||||
['run_command', 'terminal_input', 'run_python', 'runpython'].includes(item?.tool_name);
|
|
||||||
const isWritePreview = (item: any) => item?.tool_name === 'write_file';
|
|
||||||
const isRenamePreview = (item: any) => item?.tool_name === 'rename_file';
|
|
||||||
const isPathOnlyPreview = (item: any) =>
|
|
||||||
['create_file', 'create_folder', 'delete_file'].includes(item?.tool_name);
|
|
||||||
|
|
||||||
const isDeciding = (approvalId: string) =>
|
|
||||||
Array.isArray(props.decidingApprovalIds) && props.decidingApprovalIds.includes(approvalId);
|
|
||||||
|
|
||||||
const resolvePath = (item: any) => {
|
|
||||||
return item?.preview?.path || item?.preview?.file_path || item?.arguments?.path || item?.arguments?.file_path || '';
|
|
||||||
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. edit_file
|
|
||||||
- tool_call_id: functions.edit_file:168
|
|
||||||
- 参数:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"file_path": "static/src/components/panels/ToolApprovalPanel.vue",
|
|
||||||
"intent": "添加移动端判断",
|
|
||||||
"old_string": "const handleCloseClick = () => {\n console.log('[UI_DEBUG] ToolApprovalPanel close button clicked');\n emit('close');\n};\n\nconst isEditPreview = (item: any) => item?.tool_name === 'edit_file' && item?.preview?.edit_context;",
|
|
||||||
"new_string": "const handleCloseClick = () => {\n console.log('[UI_DEBUG] ToolApprovalPanel close button clicked');\n emit('close');\n};\n\n// 检测是否为移动端视口\nconst isMobileViewport = typeof window !== 'undefined' && window.innerWidth <= 768;\n\nconst isEditPreview = (item: any) => item?.tool_name === 'edit_file' && item?.preview?.edit_context;"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 结果:
|
|
||||||
```text
|
|
||||||
替换 1 处: static/src/components/panels/ToolApprovalPanel.vue
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. read_file
|
|
||||||
- tool_call_id: functions.read_file:169
|
|
||||||
- 参数:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"path": "static/src/components/panels/ToolApprovalPanel.vue",
|
|
||||||
"type": "search",
|
|
||||||
"intent": "查找文件末尾",
|
|
||||||
"query": "getEditNewLines",
|
|
||||||
"max_matches": 5
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 结果:
|
|
||||||
```text
|
|
||||||
在 static/src/components/panels/ToolApprovalPanel.vue 中搜索 "getEditNewLines",返回 2/2 条结果(不区分大小写) (max_chars=50000)
|
|
||||||
[match_1] 行 31~33 命中行: 32
|
|
||||||
```
|
|
||||||
class="diff-line diff-add"
|
|
||||||
v-for="(line, idx) in getEditNewLines(item)"
|
|
||||||
:key="`n-${item.approval_id}-${idx}`"
|
|
||||||
|
|
||||||
```
|
|
||||||
[match_2] 行 177~179 命中行: 178
|
|
||||||
```
|
|
||||||
|
|
||||||
const getEditNewLines = (item: any): string[] => {
|
|
||||||
const rows = Array.isArray(item?.preview?.edit_context?.new) ? item.preview.edit_context.new : [];
|
|
||||||
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. run_command
|
|
||||||
- tool_call_id: functions.run_command:170
|
|
||||||
- 参数:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "tail -20 static/src/components/panels/ToolApprovalPanel.vue",
|
|
||||||
"intent": "查看文件末尾",
|
|
||||||
"timeout": 10
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 结果:
|
|
||||||
```text
|
|
||||||
};
|
|
||||||
return map[name] || name || '待审批操作';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWriteLines = (item: any): string[] => {
|
|
||||||
const text = String(item?.preview?.content_preview || '');
|
|
||||||
return text.split('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEditOldLines = (item: any): string[] => {
|
|
||||||
const rows = Array.isArray(item?.preview?.edit_context?.old) ? item.preview.edit_context.old : [];
|
|
||||||
return rows.map((row: any) => String(row?.content ?? ''));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEditNewLines = (item: any): string[] => {
|
|
||||||
const rows = Array.isArray(item?.preview?.edit_context?.new) ? item.preview.edit_context.new : [];
|
|
||||||
return rows.map((row: any) => String(row?.content ?? ''));
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 用户最新的一次输入
|
|
||||||
依然有,我用的无痕模式都有,和之前一模一样
|
|
||||||
105
config/paths.py
105
config/paths.py
@ -1,4 +1,24 @@
|
|||||||
"""项目路径与目录配置。"""
|
"""项目路径与目录配置。
|
||||||
|
|
||||||
|
运行态数据(对话、用户工作区、日志等)默认存放在用户主目录下的
|
||||||
|
``~/.agents`` 中,并按运行模式分流:
|
||||||
|
|
||||||
|
- 宿主机模式(``TERMINAL_SANDBOX_MODE=host``)-> ``~/.agents/host``
|
||||||
|
- 其它模式(默认 docker / web) -> ``~/.agents/web``
|
||||||
|
|
||||||
|
对齐 ``~/.claude``、``~/.codex`` 的惯例,把运行态产物移出源码树。
|
||||||
|
|
||||||
|
路径解析优先级(从高到低):
|
||||||
|
|
||||||
|
1. 具体目录环境变量(``DATA_DIR`` / ``LOGS_DIR`` / ``USER_SPACE_DIR`` /
|
||||||
|
``API_USER_SPACE_DIR``)——单独覆盖某个目录,最高优先级;
|
||||||
|
2. 当前模式的根目录环境变量(``AGENTS_HOST_HOME`` 或 ``AGENTS_WEB_HOME``)——
|
||||||
|
整体搬迁该模式下的全部运行态数据;
|
||||||
|
3. 兜底默认值 ``~/.agents/<mode>``。
|
||||||
|
|
||||||
|
注意:``config/*.json``(custom_models、host_workspaces、auto_approval 等)属于
|
||||||
|
配置而非运行态数据,仍锚定在源码树内,不随运行态根目录迁移。
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -7,40 +27,87 @@ from pathlib import Path
|
|||||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
def _resolve_repo_path(raw_value: str, default: str) -> str:
|
def _norm(path: Path) -> Path:
|
||||||
candidate = str(raw_value or "").strip() or str(default)
|
"""展开 ``~``、转绝对路径并归一化。相对路径相对仓库根目录展开。"""
|
||||||
path = Path(candidate).expanduser()
|
path = path.expanduser()
|
||||||
if not path.is_absolute():
|
if not path.is_absolute():
|
||||||
path = (_REPO_ROOT / path).resolve()
|
path = _REPO_ROOT / path
|
||||||
else:
|
return path.resolve()
|
||||||
path = path.resolve()
|
|
||||||
return str(path)
|
|
||||||
|
|
||||||
# 默认项目路径,可通过环境变量覆盖以指向宿主机任意目录
|
|
||||||
|
def _env(name: str) -> str:
|
||||||
|
return str(os.environ.get(name, "") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_repo_path(raw_value: str, default: str) -> str:
|
||||||
|
"""解析锚定在源码树的路径(配置文件、提示词等)。
|
||||||
|
|
||||||
|
相对路径相对仓库根目录展开,绝对路径与 ``~`` 原样支持。
|
||||||
|
"""
|
||||||
|
candidate = str(raw_value or "").strip() or str(default)
|
||||||
|
return str(_norm(Path(candidate)))
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_mode() -> str:
|
||||||
|
"""运行模式:``host`` 或 ``web``。
|
||||||
|
|
||||||
|
仅当 ``TERMINAL_SANDBOX_MODE=host`` 时为 host 模式,其余(默认 docker)归为 web。
|
||||||
|
直接读环境变量,避免对 ``config.terminal`` 形成导入依赖。
|
||||||
|
"""
|
||||||
|
return "host" if _env("TERMINAL_SANDBOX_MODE").lower() == "host" else "web"
|
||||||
|
|
||||||
|
|
||||||
|
_MODE = _runtime_mode()
|
||||||
|
|
||||||
|
# 当前模式的运行态根目录。
|
||||||
|
# 优先级:模式根变量(AGENTS_HOST_HOME / AGENTS_WEB_HOME)> 默认 ~/.agents/<mode>。
|
||||||
|
_MODE_HOME_ENV = "AGENTS_HOST_HOME" if _MODE == "host" else "AGENTS_WEB_HOME"
|
||||||
|
_DEFAULT_RUNTIME_ROOT = Path("~/.agents") / _MODE
|
||||||
|
RUNTIME_ROOT = str(_norm(Path(_env(_MODE_HOME_ENV) or _DEFAULT_RUNTIME_ROOT)))
|
||||||
|
|
||||||
|
|
||||||
|
def _runtime_dir(env_name: str, subdir: str) -> str:
|
||||||
|
"""解析运行态子目录。
|
||||||
|
|
||||||
|
优先级:具体目录环境变量 > 当前模式根目录下的对应子目录。
|
||||||
|
具体目录变量支持相对路径(相对仓库根目录)、绝对路径与 ``~``。
|
||||||
|
"""
|
||||||
|
raw = _env(env_name)
|
||||||
|
if raw:
|
||||||
|
return str(_norm(Path(raw)))
|
||||||
|
return str(Path(RUNTIME_ROOT) / subdir)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 运行态目录(默认落在 ~/.agents/<mode> 下,可被各自的环境变量覆盖)──
|
||||||
|
DATA_DIR = _runtime_dir("DATA_DIR", "data")
|
||||||
|
LOGS_DIR = _runtime_dir("LOGS_DIR", "logs")
|
||||||
|
USER_SPACE_DIR = _runtime_dir("USER_SPACE_DIR", "users")
|
||||||
|
# API 专用用户与工作区(与网页用户隔离)
|
||||||
|
API_USER_SPACE_DIR = _runtime_dir("API_USER_SPACE_DIR", "api/users")
|
||||||
|
|
||||||
|
# ── 启动工作区 ──
|
||||||
|
# 默认项目路径,可通过环境变量覆盖以指向宿主机任意目录。
|
||||||
DEFAULT_PROJECT_PATH = _resolve_repo_path(os.environ.get("DEFAULT_PROJECT_PATH", ""), "./project")
|
DEFAULT_PROJECT_PATH = _resolve_repo_path(os.environ.get("DEFAULT_PROJECT_PATH", ""), "./project")
|
||||||
# 宿主机模式工作区配置文件(JSON)
|
# 兼容旧配置:若仍有模块读取 HOST_PROJECT_PATH,保留该键(实际宿主机路径选择改由 JSON 管理)。
|
||||||
HOST_WORKSPACES_FILE = _resolve_repo_path(os.environ.get("HOST_WORKSPACES_FILE", ""), "./config/host_workspaces.json")
|
|
||||||
# 兼容旧配置:若仍有模块读取 HOST_PROJECT_PATH,保留该键(实际宿主机路径选择改由 JSON 管理)
|
|
||||||
HOST_PROJECT_PATH = _resolve_repo_path(os.environ.get("HOST_PROJECT_PATH", ""), DEFAULT_PROJECT_PATH)
|
HOST_PROJECT_PATH = _resolve_repo_path(os.environ.get("HOST_PROJECT_PATH", ""), DEFAULT_PROJECT_PATH)
|
||||||
|
|
||||||
|
# ── 源码树内的配置与资源(不随运行态根目录迁移)──
|
||||||
PROMPTS_DIR = _resolve_repo_path(os.environ.get("PROMPTS_DIR", ""), "./prompts")
|
PROMPTS_DIR = _resolve_repo_path(os.environ.get("PROMPTS_DIR", ""), "./prompts")
|
||||||
DATA_DIR = _resolve_repo_path(os.environ.get("DATA_DIR", ""), "./data")
|
|
||||||
LOGS_DIR = _resolve_repo_path(os.environ.get("LOGS_DIR", ""), "./logs")
|
|
||||||
AGENT_SKILLS_DIR = _resolve_repo_path(os.environ.get("AGENT_SKILLS_DIR", ""), "./agentskills")
|
AGENT_SKILLS_DIR = _resolve_repo_path(os.environ.get("AGENT_SKILLS_DIR", ""), "./agentskills")
|
||||||
WORKSPACE_SKILLS_DIRNAME = ".agents/skills"
|
WORKSPACE_SKILLS_DIRNAME = ".agents/skills"
|
||||||
|
# 宿主机模式工作区配置文件(JSON,属于配置,锚定源码树)。
|
||||||
|
HOST_WORKSPACES_FILE = _resolve_repo_path(os.environ.get("HOST_WORKSPACES_FILE", ""), "./config/host_workspaces.json")
|
||||||
|
|
||||||
# 多用户空间
|
# ── 基于 DATA_DIR 派生的数据文件 ──
|
||||||
USER_SPACE_DIR = _resolve_repo_path(os.environ.get("USER_SPACE_DIR", ""), "./users")
|
|
||||||
USERS_DB_FILE = f"{DATA_DIR}/users.json"
|
USERS_DB_FILE = f"{DATA_DIR}/users.json"
|
||||||
INVITE_CODES_FILE = f"{DATA_DIR}/invite_codes.json"
|
INVITE_CODES_FILE = f"{DATA_DIR}/invite_codes.json"
|
||||||
ADMIN_POLICY_FILE = f"{DATA_DIR}/admin_policy.json"
|
ADMIN_POLICY_FILE = f"{DATA_DIR}/admin_policy.json"
|
||||||
|
|
||||||
# API 专用用户与工作区(与网页用户隔离)
|
|
||||||
API_USER_SPACE_DIR = _resolve_repo_path(os.environ.get("API_USER_SPACE_DIR", ""), "./api/users")
|
|
||||||
API_USERS_DB_FILE = f"{DATA_DIR}/api_users.json"
|
API_USERS_DB_FILE = f"{DATA_DIR}/api_users.json"
|
||||||
API_TOKENS_FILE = f"{DATA_DIR}/api_tokens.json"
|
API_TOKENS_FILE = f"{DATA_DIR}/api_tokens.json"
|
||||||
API_USAGE_FILE = f"{DATA_DIR}/api_usage.json"
|
API_USAGE_FILE = f"{DATA_DIR}/api_usage.json"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"RUNTIME_ROOT",
|
||||||
"DEFAULT_PROJECT_PATH",
|
"DEFAULT_PROJECT_PATH",
|
||||||
"HOST_WORKSPACES_FILE",
|
"HOST_WORKSPACES_FILE",
|
||||||
"HOST_PROJECT_PATH",
|
"HOST_PROJECT_PATH",
|
||||||
|
|||||||
@ -1,20 +1,8 @@
|
|||||||
"""子智能体相关配置。"""
|
"""子智能体相关配置。"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
from .paths import _resolve_repo_path, DATA_DIR, DEFAULT_PROJECT_PATH
|
||||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_repo_path(raw_value: str, default: str) -> str:
|
|
||||||
candidate = str(raw_value or "").strip() or str(default)
|
|
||||||
path = Path(candidate).expanduser()
|
|
||||||
if not path.is_absolute():
|
|
||||||
path = (_REPO_ROOT / path).resolve()
|
|
||||||
else:
|
|
||||||
path = path.resolve()
|
|
||||||
return str(path)
|
|
||||||
|
|
||||||
# 子智能体服务
|
# 子智能体服务
|
||||||
SUB_AGENT_SERVICE_BASE_URL = os.environ.get("SUB_AGENT_SERVICE_URL", "http://127.0.0.1:8092")
|
SUB_AGENT_SERVICE_BASE_URL = os.environ.get("SUB_AGENT_SERVICE_URL", "http://127.0.0.1:8092")
|
||||||
@ -22,9 +10,11 @@ SUB_AGENT_DEFAULT_TIMEOUT = int(os.environ.get("SUB_AGENT_DEFAULT_TIMEOUT", "180
|
|||||||
SUB_AGENT_STATUS_POLL_INTERVAL = float(os.environ.get("SUB_AGENT_STATUS_POLL_INTERVAL", "2.0"))
|
SUB_AGENT_STATUS_POLL_INTERVAL = float(os.environ.get("SUB_AGENT_STATUS_POLL_INTERVAL", "2.0"))
|
||||||
|
|
||||||
# 存储与并发限制
|
# 存储与并发限制
|
||||||
SUB_AGENT_TASKS_BASE_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_TASKS_BASE_DIR", ""), "./sub_agent/tasks")
|
# 子智能体任务目录与状态文件属于运行态数据,跟随 DATA_DIR(默认 ~/.agents/<mode>/data)。
|
||||||
SUB_AGENT_PROJECT_RESULTS_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_PROJECT_RESULTS_DIR", ""), "./project/sub_agent_results")
|
SUB_AGENT_TASKS_BASE_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_TASKS_BASE_DIR", ""), f"{DATA_DIR}/sub_agent_tasks")
|
||||||
SUB_AGENT_STATE_FILE = _resolve_repo_path(os.environ.get("SUB_AGENT_STATE_FILE", ""), "./data/sub_agents.json")
|
SUB_AGENT_STATE_FILE = _resolve_repo_path(os.environ.get("SUB_AGENT_STATE_FILE", ""), f"{DATA_DIR}/sub_agents.json")
|
||||||
|
# 子智能体产出结果有意落在工作区内,跟随 DEFAULT_PROJECT_PATH,不迁入运行态根目录。
|
||||||
|
SUB_AGENT_PROJECT_RESULTS_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_PROJECT_RESULTS_DIR", ""), f"{DEFAULT_PROJECT_PATH}/sub_agent_results")
|
||||||
SUB_AGENT_MAX_ACTIVE = int(os.environ.get("SUB_AGENT_MAX_ACTIVE", "5"))
|
SUB_AGENT_MAX_ACTIVE = int(os.environ.get("SUB_AGENT_MAX_ACTIVE", "5"))
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
339
goal_research.md
339
goal_research.md
@ -1,339 +0,0 @@
|
|||||||
# AI 编码 Agent 自主工作循环调研报告
|
|
||||||
|
|
||||||
> **一句话总结:`/goal` 命令真实存在。** Claude Code(v2.1.139, 2026年5月12日发布)和 OpenAI Codex CLI(v0.128.0+)都内置了 `/goal` 这一斜杠命令,用于设置"完成条件"让 Agent 跨多轮持续工作直到目标达成。两者实现机制不同但目标一致:打破"模型停 = 任务完"的默认假设,通过额外的验证步骤判断任务是否真正完成。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第一部分:命令真实性核查
|
|
||||||
|
|
||||||
### 1. Claude Code 的 `/goal` 命令
|
|
||||||
|
|
||||||
**✅ 真实存在。**
|
|
||||||
|
|
||||||
Claude Code 于 2026 年 5 月 12 日发布的版本 2.1.139 中引入了 `/goal` 命令。Anthropic 官方文档将其描述为:
|
|
||||||
|
|
||||||
> "Set a completion condition with /goal and Claude keeps working across turns until the condition is met."(用 /goal 设置完成条件,Claude 会跨多轮持续工作直到条件满足。)
|
|
||||||
|
|
||||||
**官方文档核心描述:**
|
|
||||||
- 用法:`/goal all tests in test/auth pass and the lint step is clean`
|
|
||||||
- 每个 turn 结束后,一个独立的轻量级 evaluator model(**默认使用 Claude Haiku**)会读取对话 transcript,检查条件是否满足
|
|
||||||
- Evaluator **不运行命令、不读取文件**,仅基于对话中已有的输出来判断
|
|
||||||
- 条件必须是**可观测的**(如测试通过、lint clean)而非模糊描述(如"让应用变得更专业")
|
|
||||||
- 可用 `/goal`(无参数)查看当前状态:已用 turn 数、token 消耗、最后 evaluator 的判断理由
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- 官方文档:https://code.claude.com/docs/en/goal
|
|
||||||
- 官方对比 `/goal` vs `/loop` vs Stop hooks 三种自主机制:同上页面的 "Compare to other autonomous workflows" 表格
|
|
||||||
- 社区分析文章(非官方,但引用了官方文档):https://blog.dailydoseofds.com/p/claude-codes-goal-command
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Codex CLI 的 `/goal` 命令
|
|
||||||
|
|
||||||
**✅ 真实存在。** 在 Codex 社区中常被称为 "Ralph Loop"。
|
|
||||||
|
|
||||||
OpenAI 官方 Cookbook 中有一篇专门的指南 "[Using Goals in Codex](https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex)",详细介绍了其用法和设计哲学。从 **Codex 0.128.0** 版本开始可用。
|
|
||||||
|
|
||||||
**官方 Cookbook 核心定义:**
|
|
||||||
> "A Goal gives Codex a completion condition: what should be true, how success should be checked, and what constraints must stay intact. ... Goals are for tasks where the next step depends on what Codex learns along the way."
|
|
||||||
|
|
||||||
**用法示例:**
|
|
||||||
```bash
|
|
||||||
/goal Reduce p95 latency below 120 ms without regressing correctness tests
|
|
||||||
```
|
|
||||||
|
|
||||||
**生命周期管理:**
|
|
||||||
- `/goal` — 查看当前 Goal
|
|
||||||
- `/goal pause` — 暂停
|
|
||||||
- `/goal resume` — 恢复
|
|
||||||
- `/goal clear` — 清除
|
|
||||||
|
|
||||||
**官方推荐 Goal 写作六要素:**
|
|
||||||
1. 期望结束状态
|
|
||||||
2. 验证方式(具体证据)
|
|
||||||
3. 约束条件(什么不能改变)
|
|
||||||
4. 允许的输入/工具/边界
|
|
||||||
5. 各轮次之间如何选择下一步
|
|
||||||
6. 被阻塞时如何报告
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- 官方 Cookbook:https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex
|
|
||||||
- 官方 CLI 参考:https://developers.openai.com/codex/cli/reference
|
|
||||||
- OpenAI 官方博客(Agent Loop 技术细节):https://openai.com/index/unrolling-the-codex-agent-loop
|
|
||||||
- 社区评测(非官方):https://kingy.ai/ai/openai-codex-goal-the-new-long-horizon-mode-for-agentic-coding
|
|
||||||
- GitHub 讨论(非官方):https://github.com/openai/codex/discussions/22749
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 结论来源汇总
|
|
||||||
|
|
||||||
| 结论 | 来源类型 | 链接 |
|
|
||||||
|------|---------|------|
|
|
||||||
| Claude Code `/goal` 真实存在 | 官方文档 | https://code.claude.com/docs/en/goal |
|
|
||||||
| Codex CLI `/goal` 真实存在 | 官方 Cookbook | https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex |
|
|
||||||
| Claude Code `/goal` 使用 Haiku 做 evaluator | 社区分析(引用官方文档) | https://blog.dailydoseofds.com/p/claude-codes-goal-command |
|
|
||||||
| Codex 从 v0.128.0 起支持 Goals | 官方 Cookbook | 同上 Cookbook |
|
|
||||||
| Codex Agent Loop 技术细节 | OpenAI 官方博客(Michael Bolin 撰写) | https://openai.com/index/unrolling-the-codex-agent-loop |
|
|
||||||
| Codex 开源代码仓库 | GitHub(官方) | https://github.com/openai/codex |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第二部分:自主循环的实现机制(核心)
|
|
||||||
|
|
||||||
### 4. Agent 如何判断"任务还没做完、需要继续"?
|
|
||||||
|
|
||||||
这是核心技术问题。两个工具采用了不同的方案:
|
|
||||||
|
|
||||||
#### Claude Code `/goal` 的循环控制逻辑
|
|
||||||
|
|
||||||
1. **每个 turn 结束后触发 evaluator**:Claude 完成一个 turn(读取文件、运行命令、编辑代码等工具调用完成并输出 assistant message),evaluator model 被调用
|
|
||||||
2. **Evaluator 接收**:完整的对话 transcript + 用户设置的完成条件
|
|
||||||
3. **Evaluator 判断**:条件是否已满足(基于 transcript 中的证据)
|
|
||||||
4. **不满足 → 自动开启新 turn**:条件未满足时,Claude 继续工作
|
|
||||||
5. **满足 → 停止**:evaluator 判断条件达成,循环结束
|
|
||||||
|
|
||||||
**关键设计**:Evaluator model 是**独立的、轻量级的模型调用**(默认 Claude Haiku),不共享主工作模型的上下文。它仅做"裁判",不参与实际工作。这是打破"模型停止输出 = 任务完成"假设的核心机制。
|
|
||||||
|
|
||||||
#### Codex `/goal` 的循环控制逻辑
|
|
||||||
|
|
||||||
Codex 的实现更加工程化,它是一个 **thread-scoped(线程级)的持久化状态机**:
|
|
||||||
|
|
||||||
1. **Goal 作为持久化线程状态**:Goal 不是全局内存或项目指令,而是绑定到当前线程的持久状态记录(包含目标、生命周期、预算、进度)
|
|
||||||
2. **事件驱动的 continuation**:Codex 仅在安全的边界检查是否应继续:
|
|
||||||
- 上一个 turn 已完成
|
|
||||||
- 没有其他工作正在 pending
|
|
||||||
- 没有用户输入排队
|
|
||||||
- 线程处于空闲状态
|
|
||||||
3. **Plan-only 不触发继续**:如果上一个 turn 只是"计划模式"(只读),不会自动触发下一轮
|
|
||||||
4. **空转抑制**:如果 continuation turn 没有做任何 tool call,下一次自动继续被抑制,防止空转烧钱
|
|
||||||
5. **基于证据的完成判定**:Codex 必须将"目标 vs 具体证据"进行对比——修改的文件、运行的命令输出、测试结果、benchmark 输出等
|
|
||||||
|
|
||||||
#### 两种方案的本质差异
|
|
||||||
|
|
||||||
| 维度 | Claude Code /goal | Codex /goal |
|
|
||||||
|------|-------------------|-------------|
|
|
||||||
| 完成判定者 | 独立 evaluator model(Haiku) | 主工作模型 + 硬编码规则 |
|
|
||||||
| 判定依据 | 对话 transcript | 对话 transcript + 硬编码状态检查 |
|
|
||||||
| 继续触发 | evaluator 判断"未完成" | 事件驱动(idle + active + no input) |
|
|
||||||
| 空转防护 | 依赖 evaluator 判断 | 硬编码抑制(无 tool call 则停止下次自动继续) |
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- Claude Code 官方文档(evaluator 说明):https://code.claude.com/docs/en/goal
|
|
||||||
- Claude Code /goal 社区深度分析:https://blog.dailydoseofds.com/p/claude-codes-goal-command
|
|
||||||
- Codex 官方 Cookbook(架构设计详解,含架构图):https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex
|
|
||||||
- Codex Agent Loop 官方博客(turn 概念):https://openai.com/index/unrolling-the-codex-agent-loop
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. "模型停止输出文本 = 任务完成"的默认假设,它们是怎么打破的?
|
|
||||||
|
|
||||||
#### Claude Code:独立的 Evaluator 审查步骤
|
|
||||||
|
|
||||||
- **不是模型自己说"我做完了"**:主工作模型完成 turn 后输出 assistant message(如"我已经修好了 bug"),但这不等同于 Goal 完成
|
|
||||||
- **Evaluator model 是独立的审查者**:它是一个独立的模型调用(默认 Haiku),只读取对话 transcript,对照完成条件做判断
|
|
||||||
- **模型"相信"不算,证据才算**:官方文档要求条件必须基于可观测输出(如测试通过、lint clean),排斥模糊条件(如"看起来很棒")
|
|
||||||
|
|
||||||
#### Codex:硬编码的"证据审计"步骤
|
|
||||||
|
|
||||||
- **must be evidence-based**(必须基于证据):官方 Cookbook 反复强调,"A Goal should not be marked complete because the model believes it is probably done"
|
|
||||||
- **多层次的验证**:
|
|
||||||
- 硬编码检查:文件变更、命令执行结果、测试输出
|
|
||||||
- 模型辅助判断:对比目标与证据,给出是否完成的判断
|
|
||||||
- **每轮结束后都有一个"检查点"**:Codex 在 turn 完成后检查 Goal 状态,而不是模型随时可以声明"完成"
|
|
||||||
- **预算耗尽时明确区分**:budget 耗尽 ≠ 目标完成,Codex 必须区分两者并总结 blocker
|
|
||||||
|
|
||||||
#### 关键差异
|
|
||||||
|
|
||||||
| 审查方式 | Claude Code | Codex |
|
|
||||||
|---------|-------------|-------|
|
|
||||||
| 审查者 | 独立 evaluator model | 主模型 + 硬编码规则 |
|
|
||||||
| 审查时机 | 每个 turn 后 | 每个 turn 后的 idle 检查点 |
|
|
||||||
| 证据要求 | 对话中可见的输出 | 对话中的证据 + 系统状态 |
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- Claude Code 官方文档 How evaluation works:https://code.claude.com/docs/en/goal
|
|
||||||
- 社区文章详解 evaluator loop:https://blog.dailydoseofds.com/p/claude-codes-goal-command
|
|
||||||
- Codex Cookbook "How Goals are designed" 章节:https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex
|
|
||||||
- OpenAI 安全博客 Auto-review(相关机制):https://alignment.openai.com/auto-review
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. 是否用到 plan / todo / task list 结构来追踪进度?
|
|
||||||
|
|
||||||
#### Claude Code
|
|
||||||
|
|
||||||
- Claude Code 的 Agent SDK 有内置的 **TaskCreate / TaskUpdate** 工具(在 SDK 工具列表中明确列出)
|
|
||||||
- `/goal` 的官方文档**未明确提到**必须使用 task list 来驱动循环;evaluator 直接判断条件是否满足
|
|
||||||
- 但从 SDK 文档看,task 工具是可用的编排能力(https://code.claude.com/docs/en/agent-sdk/agent-loop)
|
|
||||||
|
|
||||||
#### Codex
|
|
||||||
|
|
||||||
- Codex 有内置的 **`update_plan`** 工具(在 OpenAI 官方博客的 Agent Loop 文章中有代码示例)
|
|
||||||
- 对于 Goal 模式,Cookbook 的架构描述中提到 "Goal records the objective, lifecycle, budget, and **progress accounting**"
|
|
||||||
- Goal 本身作为 thread 的状态,承担了"进度追踪"的角色
|
|
||||||
- 使用模式是**"目标 + 当前证据 + 下一步"**,而非严格的 checklist 式 todo
|
|
||||||
|
|
||||||
#### 结论
|
|
||||||
|
|
||||||
两个工具都没有将"todo list 全部勾选"作为循环继续/停止的唯一依据。而是采用**"条件 vs 证据"**的判定模式。Plan/task 工具是辅助模型组织工作的手段,不是硬编码的循环控制逻辑。
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- Claude Code SDK 工具列表(含 TaskCreate/TaskUpdate):https://code.claude.com/docs/en/agent-sdk/agent-loop
|
|
||||||
- Codex blog Agent Loop 中 update_plan 工具代码:https://openai.com/index/unrolling-the-codex-agent-loop
|
|
||||||
- Codex Cookbook How Goals are designed:https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. 防止死循环和失控的机制
|
|
||||||
|
|
||||||
#### Claude Code `/goal`
|
|
||||||
|
|
||||||
| 机制 | 详情 | 来源 |
|
|
||||||
|------|------|------|
|
|
||||||
| **无内置 token budget** | `/goal` 本身没有 token 或金钱上限;官方建议在条件中显式加 `stop after 20 turns` | https://code.claude.com/docs/en/goal |
|
|
||||||
| **Agent SDK 层 max_turns** | 通过 SDK 编程使用时,可设置 `max_turns` 和 `max_budget_usd` | https://code.claude.com/docs/en/agent-sdk/agent-loop |
|
|
||||||
| **手动中断** | `Ctrl+C` 或 `/goal clear` 随时中断 | 同上 |
|
|
||||||
| **Trust dialog 限制** | `/goal` 仅在用户接受信任对话框的工作区可用 | 同上 |
|
|
||||||
| **Hooks 限制** | `disableAllHooks` 或 `allowManagedHooksOnly` 设置时 `/goal` 不可用 | 同上 |
|
|
||||||
| **Context window 管理** | SDK 支持自动 compaction(`PreCompact` hook),防止上下文溢出 | https://code.claude.com/docs/en/agent-sdk/agent-loop |
|
|
||||||
|
|
||||||
#### Codex `/goal`
|
|
||||||
|
|
||||||
| 机制 | 详情 | 来源 |
|
|
||||||
|------|------|------|
|
|
||||||
| **Budget accounting(预算核算)** | Goal 状态中包含预算;达到预算时停止实质性工作,总结进度和 blockers | https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex |
|
|
||||||
| **空转抑制** | 如果 continuation turn 未做任何 tool call,下一次自动继续被抑制 | 同上 |
|
|
||||||
| **Safe boundary 检查** | 仅在 turn 完成 + 线程空闲 + 无用户输入排队时检查是否需要继续 | 同上 |
|
|
||||||
| **手动生命周期控制** | `/goal pause`(暂停)、`/goal resume`(恢复)、`/goal clear`(清除) | 同上 |
|
|
||||||
| **Auto-compaction** | 超过 `auto_compact_limit` 时自动压缩上下文 | https://openai.com/index/unrolling-the-codex-agent-loop |
|
|
||||||
| **Prompt caching** | 通过精确 prompt 前缀匹配实现缓存,降低长期运行成本 | 同上 |
|
|
||||||
|
|
||||||
#### 共同模式
|
|
||||||
|
|
||||||
两者都没有"检测到重复输出自动停止"的硬编码机制——这仍然依赖模型自身判断和 evaluator/证据检查。但两者都有**多层"软硬"防护**:预算限制、turn 数限制、空转抑制、手动中断点。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. AGENTS.md / CLAUDE.md / Plan Mode / Hooks 在自主循环中的角色
|
|
||||||
|
|
||||||
#### Codex 的 AGENTS.md
|
|
||||||
|
|
||||||
- **注入到初始 prompt 中**:AGENTS.md 的内容作为 `role=user` 的消息注入到 Responses API 的 `input` 中(官方博客 Agent Loop 文章第3步)
|
|
||||||
- **层级结构**:
|
|
||||||
1. `$CODEX_HOME/AGENTS.override.md` 或 `AGENTS.md`(全局)
|
|
||||||
2. 从 Git/project root 向上到 `cwd`:读取 `AGENTS.override.md`、`AGENTS.md` 或 `project_doc_fallback_filenames` 中配置的文件
|
|
||||||
- **在 Goal 模式中的作用**:AGENTS.md 在每次模型调用中都会被包含(作为 prompt 前缀),提供项目规范、代码风格、验收标准等"持久上下文",确保模型在长达 N 个 turn 的自主运行中保持一致的行为
|
|
||||||
- **Skills 系统**:如果配置了 skills,skill metadata 和如何使用 skills 的说明也会被注入
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- 官方博客 Agent Loop "Building the initial prompt" 第3步:https://openai.com/index/unrolling-the-codex-agent-loop
|
|
||||||
- 官方 AGENTS.md 指南:https://developers.openai.com/codex/guides/agents-md
|
|
||||||
|
|
||||||
#### Claude Code 的 CLAUDE.md
|
|
||||||
|
|
||||||
- **通过 settingSources 加载**:CLAUDE.md 在 session 开始时被加载,其完整内容在每次 API 请求中都被包含(但受益于 prompt caching,只有第一次请求支付完整成本)
|
|
||||||
- **在 Goal 模式中的作用**:
|
|
||||||
- CLAUDE.md 定义架构决策、编码规范、验收标准
|
|
||||||
- 确保模型在 30+ 个 turn 的长运行中保持一致的上下文理解
|
|
||||||
- 可包含 summarization 指令,指导自动 compaction 时保留关键信息
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- Claude Code Agent Loop SDK 文档"What consumes context":https://code.claude.com/docs/en/agent-sdk/agent-loop
|
|
||||||
- 社区最佳实践:https://blog.dailydoseofds.com/p/claude-codes-goal-command
|
|
||||||
|
|
||||||
#### Claude Code 的 Plan Mode(plan mode)
|
|
||||||
|
|
||||||
- SDK 中有 `permission_mode: "plan"`,在此模式下只允许只读工具运行,Claude 只探索和产出计划而不编辑文件
|
|
||||||
- 官方 `/goal` 文档未明确说明 plan mode 与 goal 的交互,但 SDK 中 plan mode 是独立概念
|
|
||||||
- 来自非官方来源的分析提到 plan-only 工作不触发 Goal 的 continuation
|
|
||||||
|
|
||||||
#### Claude Code 的 Hooks 机制
|
|
||||||
|
|
||||||
官方 SDK 文档列出了完整的 hooks 体系:
|
|
||||||
|
|
||||||
| Hook | 触发时机 | 在 Goal 循环中的作用 |
|
|
||||||
|------|---------|-------------------|
|
|
||||||
| `PreToolUse` | 工具执行前 | 验证输入、阻止危险命令 |
|
|
||||||
| `PostToolUse` | 工具返回后 | **自动运行 lint / type-check**,在运行中发现错误 |
|
|
||||||
| `Stop` | Agent 完成时 | 验证结果、保存 session 状态 |
|
|
||||||
| `PreCompact` | 上下文压缩前 | 归档完整 transcript 后再摘要 |
|
|
||||||
|
|
||||||
Hooks 与 `/goal` 配合使用时,可以在每次文件编辑后自动做验证(PostToolUse hook),减少返工。
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- Claude Code Agent Loop SDK Hooks 文档:https://code.claude.com/docs/en/agent-sdk/agent-loop
|
|
||||||
- Claude Code Hooks 参考:https://code.claude.com/docs/en/hooks
|
|
||||||
|
|
||||||
#### Claude Code 的 Auto Mode
|
|
||||||
|
|
||||||
- Auto mode 是独立的权限模式(`permission_mode: "auto"`),用模型分类器自动审批/拒绝每个工具调用
|
|
||||||
- 两层分类:快速初筛 + 深度分析(处理不确定的危险操作)
|
|
||||||
- **Auto mode + /goal 是最佳组合**:Auto mode 让每个 turn 不被权限确认打断,/goal 让多个 turn 自动衔接
|
|
||||||
|
|
||||||
**来源:**
|
|
||||||
- Anthropic 官方博客 Auto Mode:https://www.anthropic.com/engineering/claude-code-auto-mode
|
|
||||||
- InfoQ 分析文章:https://www.infoq.com/news/2026/05/anthropic-claude-code-auto-mode
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第三部分:同类开源方案(补充参考)
|
|
||||||
|
|
||||||
### 9. 其他做"自主循环直到完成"的方案
|
|
||||||
|
|
||||||
#### Aider
|
|
||||||
- **完成判定**:Aider 的默认模式是"一次请求一次回复",但可通过配置实现循环。它主要依靠模型自身的判断(模型输出"任务完成")来结束。没有独立的 evaluator。
|
|
||||||
- **自我验证**:依赖用户提供测试命令(`/test`),Aider 在每次修改后自动运行测试,测试失败则自动尝试修复。这形成了一种"修改→测试→修复"的内循环。
|
|
||||||
- **链接**:https://github.com/paul-gauthier/aider
|
|
||||||
|
|
||||||
#### OpenHands(原 OpenDevin)
|
|
||||||
- **完成判定**:OpenHands 使用 agent 自己判断任务是否完成(模型声明完成)。但社区最佳实践建议在 prompt 中要求 agent "运行测试套件后再声明完成"。
|
|
||||||
- **自我验证**:强调 "verification loop"——agent 被提示在完成任务前必须运行测试/验证命令并展示通过证据。这不是硬编码的,而是 prompt engineering 的结果。
|
|
||||||
- **链接**:https://github.com/All-Hands-AI/OpenHands | 深度分析:https://dev.to/truongpx396/openhands-deep-dive-build-your-own-guide-1al0
|
|
||||||
|
|
||||||
#### SWE-agent
|
|
||||||
- **完成判定**:SWE-agent 使用固定步数限制(如 30 步)或模型自行判断。没有独立的完成条件 evaluator。
|
|
||||||
- **自我验证**:SWE-agent 的核心创新是其 Agent-Computer Interface (ACI)——精心设计的工具和反馈格式。它通过让模型查看命令执行结果来做判断,但没有额外的验证层。
|
|
||||||
- **已转向更简化的 mini-SWE-agent**:当前主要开发精力已转移到 mini-swe-agent。
|
|
||||||
- **链接**:https://github.com/SWE-agent/SWE-agent | 论文:https://arxiv.org/abs/2405.15793
|
|
||||||
|
|
||||||
#### AutoGPT
|
|
||||||
- **完成判定**:AutoGPT 是早期"自主循环"的典型代表。它使用**任务列表(task list)机制**:主 agent 将目标分解为子任务,逐一执行并勾选,直到列表清空。
|
|
||||||
- **自我验证**:每个子任务执行后,agent 会生成新的子任务并重新排序优先级。这是一种"任务队列为空 = 完成"的模式,没有独立的验证步骤。容易出现死循环(agent 不断生成新任务)。
|
|
||||||
- **链接**:https://github.com/Significant-Gravitas/AutoGPT
|
|
||||||
|
|
||||||
#### 总结对比
|
|
||||||
|
|
||||||
| 方案 | 完成判定 | 验证机制 | 独立 evaluator |
|
|
||||||
|------|---------|---------|---------------|
|
|
||||||
| Claude Code /goal | 独立 Haiku evaluator | 模型审查 transcript | ✅ 有 |
|
|
||||||
| Codex /goal | 主模型 + 硬编码规则 | 证据 vs 目标审计 | ⚠️ 半独立(模型+规则) |
|
|
||||||
| Aider | 模型自判断 | 用户提供的测试命令 | ❌ 无 |
|
|
||||||
| OpenHands | 模型自判断 | Prompt 引导的验证 | ❌ 无 |
|
|
||||||
| SWE-agent | 模型自判断 + 步数限制 | 工具输出反馈 | ❌ 无 |
|
|
||||||
| AutoGPT | 任务列表清空 | 任务队列管理 | ❌ 无 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 附录:对你设计自己 Agent 项目的建议
|
|
||||||
|
|
||||||
基于以上调研,Claude Code 和 Codex 的 `/goal` 设计可以提炼出以下**可复用的设计原则**:
|
|
||||||
|
|
||||||
1. **分离"工作者"和"裁判"**:Claude Code 的独立 evaluator model 是最优雅的方案。即使不用独立模型,至少应该有一个"审查步骤",不让工作模型自己声明"我做完了"。
|
|
||||||
|
|
||||||
2. **证据驱动,而非信心驱动**:两个工具都强调"条件必须是可观测的"。设计你的 Goal 系统时,要求用户定义可被自动化验证的完成标准(如测试通过、构建成功、文件存在等)。
|
|
||||||
|
|
||||||
3. **硬编码安全边界**:Codex 的空转抑制和 event-driven continuation 值得借鉴。不要让 Agent 无限制地自循环——设置 turn 上限、空转检测、预算上限。
|
|
||||||
|
|
||||||
4. **分层权限**:Claude Code 的 Auto mode(模型分类器审批)+ Goal 的组合模式很实用。设计时考虑"什么操作可以自动审批"和"什么操作需要人类确认"。
|
|
||||||
|
|
||||||
5. **持久化上下文很重要**:AGENTS.md / CLAUDE.md 在长运行中提供一致性。你的系统也需要类似的"项目规范注入"机制。
|
|
||||||
|
|
||||||
6. **可中断 + 可恢复**:两者都支持 pause/resume/clear 生命周期。长时间运行的 Agent 必须支持这些操作。
|
|
||||||
|
|
||||||
7. **即使没有单独 evaluator model,也可以在 prompt 中实现"审查步骤"**:在每轮结束后,向模型发送一个特殊的审查 prompt("请基于以上对话中可见的证据,判断以下条件是否已满足..."),这本质上就是 Codex 当前的做法。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **报告完成时间**:2026年5月29日
|
|
||||||
> **调研工具**:web_search + extract_webpage
|
|
||||||
> **声明**:所有标注为"官方"的结论均来源于官方文档、GitHub 仓库或官方博客;标注为"社区/非官方"的来源于第三方博客或分析文章。未找到的信息已明确标注"未找到"。
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
"""Deepseek API 流式快速验证脚本。
|
|
||||||
|
|
||||||
按照需求:
|
|
||||||
- deepseek-chat: max_tokens=8000
|
|
||||||
- deepseek-reasoner: max_tokens=65536
|
|
||||||
- 开启流式输出,打印每条 data 行便于确认返回。
|
|
||||||
|
|
||||||
根据用户允许,直接在脚本中硬编码 DeepSeek 测试用 API 信息,避免被仓库中其他
|
|
||||||
默认配置(如 kimi)覆盖。请勿在生产使用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
|
|
||||||
DEEPSEEK_BASE_URL = "https://api.deepseek.com"
|
|
||||||
DEEPSEEK_API_KEY = "sk-3457fbc33f0b4aefb2ce1d3101bb2341"
|
|
||||||
DEEPSEEK_CHAT_MODEL = "deepseek-chat"
|
|
||||||
DEEPSEEK_REASONER_MODEL = "deepseek-reasoner"
|
|
||||||
|
|
||||||
|
|
||||||
def build_headers(api_key: str) -> dict[str, str]:
|
|
||||||
return {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_call(
|
|
||||||
*,
|
|
||||||
name: str,
|
|
||||||
base_url: str,
|
|
||||||
api_key: str,
|
|
||||||
model: str,
|
|
||||||
max_tokens: int,
|
|
||||||
prompt: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
"""向指定模型发起流式 chat/completions 请求并打印 data 行。"""
|
|
||||||
url = base_url.rstrip("/") + "/chat/completions"
|
|
||||||
payload = {
|
|
||||||
"model": model,
|
|
||||||
"stream": True,
|
|
||||||
"max_tokens": max_tokens,
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": prompt
|
|
||||||
or f"这是 {name} 模型的流式测试,请用一句话自我介绍。",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"\n=== {name} ===")
|
|
||||||
print(f"POST {url}")
|
|
||||||
async with httpx.AsyncClient(http2=True, timeout=120) as client:
|
|
||||||
async with client.stream(
|
|
||||||
"POST",
|
|
||||||
url,
|
|
||||||
json=payload,
|
|
||||||
headers=build_headers(api_key),
|
|
||||||
) as resp:
|
|
||||||
print(f"status: {resp.status_code}")
|
|
||||||
if resp.status_code != 200:
|
|
||||||
body = await resp.aread()
|
|
||||||
print("error body:", body.decode(errors="ignore"))
|
|
||||||
return
|
|
||||||
async for line in resp.aiter_lines():
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith("data:"):
|
|
||||||
data = line[5:].strip()
|
|
||||||
if data == "[DONE]":
|
|
||||||
print("[DONE]")
|
|
||||||
break
|
|
||||||
print(data)
|
|
||||||
else:
|
|
||||||
# 兼容潜在的非 data 行(例如心跳)
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
|
||||||
await stream_call(
|
|
||||||
name="deepseek-chat (max_tokens=8000)",
|
|
||||||
base_url=DEEPSEEK_BASE_URL,
|
|
||||||
api_key=DEEPSEEK_API_KEY,
|
|
||||||
model=DEEPSEEK_CHAT_MODEL,
|
|
||||||
max_tokens=8000,
|
|
||||||
)
|
|
||||||
|
|
||||||
await stream_call(
|
|
||||||
name="deepseek-reasoner (max_tokens=65536)",
|
|
||||||
base_url=DEEPSEEK_BASE_URL,
|
|
||||||
api_key=DEEPSEEK_API_KEY,
|
|
||||||
model=DEEPSEEK_REASONER_MODEL,
|
|
||||||
max_tokens=65536,
|
|
||||||
prompt="你是一个思考模型,请简述测试状态并结束。",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
"""Qwen API 流式测试脚本(qwen-max,快速模型)。
|
|
||||||
|
|
||||||
目标:
|
|
||||||
- 验证 qwen-max 的流式输出与用量字段(无思考能力)。
|
|
||||||
- 打印 data 行,尾包包含 usage。
|
|
||||||
|
|
||||||
注意:硬编码测试密钥,仅限本地验证,勿用于生产。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
|
|
||||||
QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
||||||
QWEN_API_KEY = "sk-64af1343e67d46d7a902ef5bcf6817ad"
|
|
||||||
QWEN_MAX_MODEL = "qwen3-max"
|
|
||||||
|
|
||||||
|
|
||||||
def headers(api_key: str) -> dict[str, str]:
|
|
||||||
return {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_call(
|
|
||||||
*,
|
|
||||||
base_url: str,
|
|
||||||
api_key: str,
|
|
||||||
model: str,
|
|
||||||
max_tokens: int = 2048,
|
|
||||||
prompt: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
url = base_url.rstrip("/") + "/chat/completions"
|
|
||||||
payload = {
|
|
||||||
"model": model,
|
|
||||||
"stream": True,
|
|
||||||
"max_tokens": max_tokens,
|
|
||||||
"stream_options": {"include_usage": True},
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": prompt
|
|
||||||
or "请用简短中文自我介绍,并说明你当前正在执行的动作。",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
print("\n=== qwen-max fast mode ===")
|
|
||||||
print(f"POST {url}")
|
|
||||||
async with httpx.AsyncClient(http2=True, timeout=120) as client:
|
|
||||||
async with client.stream(
|
|
||||||
"POST", url, json=payload, headers=headers(api_key)
|
|
||||||
) as resp:
|
|
||||||
print("status:", resp.status_code)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
body = await resp.aread()
|
|
||||||
print("error body:", body.decode(errors="ignore"))
|
|
||||||
return
|
|
||||||
async for line in resp.aiter_lines():
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith("data:"):
|
|
||||||
data = line[5:].strip()
|
|
||||||
if data == "[DONE]":
|
|
||||||
print("[DONE]")
|
|
||||||
break
|
|
||||||
print(data)
|
|
||||||
else:
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
|
||||||
await stream_call(
|
|
||||||
base_url=QWEN_BASE_URL,
|
|
||||||
api_key=QWEN_API_KEY,
|
|
||||||
model=QWEN_MAX_MODEL,
|
|
||||||
max_tokens=64000, # qwen3-max 官方上限 64K
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
"""Qwen VL 测试脚本(兼容模式)。
|
|
||||||
|
|
||||||
用例:
|
|
||||||
1) 纯文字:验证流式输出与 usage。
|
|
||||||
2) 图文:发送本地图片,验证多模态输入。
|
|
||||||
|
|
||||||
注意:硬编码测试密钥,仅限本地验证,勿用于生产。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import base64
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
|
|
||||||
QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
||||||
QWEN_API_KEY = "sk-64af1343e67d46d7a902ef5bcf6817ad"
|
|
||||||
QWEN_VL_MODEL = "qwen3-vl-plus"
|
|
||||||
|
|
||||||
# 默认图片路径(仓库根目录下“截图/截屏2025-12-12 17.30.04.png”)
|
|
||||||
DEFAULT_IMAGE_PATH = Path("截图/截屏2025-12-12 17.30.04.png")
|
|
||||||
|
|
||||||
|
|
||||||
def headers(api_key: str) -> dict[str, str]:
|
|
||||||
return {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_image_content(image_path: Path) -> str:
|
|
||||||
data = image_path.read_bytes()
|
|
||||||
b64 = base64.b64encode(data).decode("ascii")
|
|
||||||
return f"data:image/{image_path.suffix.lstrip('.').lower()};base64,{b64}"
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_call(
|
|
||||||
*,
|
|
||||||
name: str,
|
|
||||||
base_url: str,
|
|
||||||
api_key: str,
|
|
||||||
model: str,
|
|
||||||
messages,
|
|
||||||
max_tokens: int,
|
|
||||||
enable_thinking: bool = False,
|
|
||||||
) -> None:
|
|
||||||
url = base_url.rstrip("/") + "/chat/completions"
|
|
||||||
payload = {
|
|
||||||
"model": model,
|
|
||||||
"stream": True,
|
|
||||||
"max_tokens": max_tokens,
|
|
||||||
"messages": messages,
|
|
||||||
"stream_options": {"include_usage": True},
|
|
||||||
}
|
|
||||||
if enable_thinking:
|
|
||||||
payload["enable_thinking"] = True
|
|
||||||
print(f"\n=== {name} ===")
|
|
||||||
print(f"POST {url}")
|
|
||||||
async with httpx.AsyncClient(http2=True, timeout=180) as client:
|
|
||||||
async with client.stream(
|
|
||||||
"POST", url, json=payload, headers=headers(api_key)
|
|
||||||
) as resp:
|
|
||||||
print("status:", resp.status_code)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
body = await resp.aread()
|
|
||||||
print("error body:", body.decode(errors="ignore"))
|
|
||||||
return
|
|
||||||
async for line in resp.aiter_lines():
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith("data:"):
|
|
||||||
data = line[5:].strip()
|
|
||||||
if data == "[DONE]":
|
|
||||||
print("[DONE]")
|
|
||||||
break
|
|
||||||
print(data)
|
|
||||||
else:
|
|
||||||
print(line)
|
|
||||||
|
|
||||||
|
|
||||||
async def main(image_path: Optional[Path] = None) -> None:
|
|
||||||
# 1) 纯文字
|
|
||||||
text_messages = [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "请用一句话自我介绍,并简单说明你目前在执行的动作。",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
await stream_call(
|
|
||||||
name="qwen-vl text only",
|
|
||||||
base_url=QWEN_BASE_URL,
|
|
||||||
api_key=QWEN_API_KEY,
|
|
||||||
model=QWEN_VL_MODEL,
|
|
||||||
messages=text_messages,
|
|
||||||
max_tokens=32000, # 官方上限 32K
|
|
||||||
enable_thinking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2) 图文
|
|
||||||
img_path = image_path or DEFAULT_IMAGE_PATH
|
|
||||||
if not img_path.exists():
|
|
||||||
print(f"\n[warn] 图片文件不存在: {img_path}")
|
|
||||||
return
|
|
||||||
img_url = build_image_content(img_path)
|
|
||||||
multimodal_messages = [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "text", "text": "请描述这张图片的主要内容,并给出一句话总结。"},
|
|
||||||
{"type": "image_url", "image_url": {"url": img_url}},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
await stream_call(
|
|
||||||
name="qwen-vl image+text",
|
|
||||||
base_url=QWEN_BASE_URL,
|
|
||||||
api_key=QWEN_API_KEY,
|
|
||||||
model=QWEN_VL_MODEL,
|
|
||||||
messages=multimodal_messages,
|
|
||||||
max_tokens=32000, # 官方上限 32K
|
|
||||||
enable_thinking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
@ -1,2 +0,0 @@
|
|||||||
hello
|
|
||||||
world
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import asyncio, sys, os, copy
|
|
||||||
from pathlib import Path
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
sys.path.insert(0, str(ROOT))
|
|
||||||
from utils.api_client import DeepSeekClient
|
|
||||||
|
|
||||||
class FakeClient(DeepSeekClient):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(thinking_mode=True, web_mode=True)
|
|
||||||
self.sent = []
|
|
||||||
self.call_idx = 0
|
|
||||||
async def chat(self, messages, tools=None, stream=True):
|
|
||||||
self.sent.append(copy.deepcopy(messages))
|
|
||||||
self.call_idx += 1
|
|
||||||
if self.call_idx == 1:
|
|
||||||
yield {
|
|
||||||
"choices": [
|
|
||||||
{"delta": {
|
|
||||||
"reasoning_content": "think1 ",
|
|
||||||
"tool_calls": [
|
|
||||||
{"id": "call_1", "index": 0, "type": "function", "function": {"name": "foo", "arguments": "{}"}}
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
yield {"choices": [{"delta": {}}]}
|
|
||||||
else:
|
|
||||||
yield {"choices": [{"delta": {"content": "done"}}]}
|
|
||||||
yield {"choices": [{"delta": {}}]}
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
client = FakeClient()
|
|
||||||
messages = [
|
|
||||||
{"role": "system", "content": "sys"},
|
|
||||||
{"role": "user", "content": "hi"}
|
|
||||||
]
|
|
||||||
async def tool_handler(name, args):
|
|
||||||
return '{}'
|
|
||||||
out = await client.chat_with_tools(messages, tools=[{"type":"function","function":{"name":"foo","parameters":{"type":"object","properties":{}}}}], tool_handler=tool_handler)
|
|
||||||
print('final', out)
|
|
||||||
import json
|
|
||||||
for i, m in enumerate(client.sent, 1):
|
|
||||||
print('\ncall', i)
|
|
||||||
print(json.dumps(m, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.run(main())
|
|
||||||
@ -1,183 +0,0 @@
|
|||||||
"""
|
|
||||||
手工验证 Qwen-VL 在多轮工具调用时是否保留 reasoning_content。
|
|
||||||
|
|
||||||
步骤:
|
|
||||||
1. 首次请求:用户要求“在思考中想一个 8 位数字但不要直接输出,先调用天气工具,再告诉我数字”。
|
|
||||||
2. 模型若返回 tool_calls,我们本地模拟 weather 查询,把结果作为 tool 消息返回。
|
|
||||||
3. 第二次请求:带上第一次 assistant 工具调用 + tool 结果,检查模型是否能输出第一次思考里的数字。
|
|
||||||
|
|
||||||
运行:
|
|
||||||
export API_BASE_QWEN="https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
||||||
export API_KEY_QWEN="sk-..."
|
|
||||||
python3 scratch_test/qwen_vl_reasoning_test.py
|
|
||||||
|
|
||||||
日志:
|
|
||||||
输出到 logs/qwen_vl_reasoning_test_<timestamp>.json
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Any
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
# 读取模型配置(若未设置环境变量则使用默认)
|
|
||||||
API_BASE = os.environ.get("API_BASE_QWEN", "https://dashscope.aliyuncs.com/compatible-mode/v1").rstrip("/")
|
|
||||||
API_KEY = os.environ.get("API_KEY_QWEN", os.environ.get("DASHSCOPE_API_KEY", ""))
|
|
||||||
MODEL_ID = os.environ.get("MODEL_QWEN_VL", "qwen3-vl-plus")
|
|
||||||
|
|
||||||
LOG_DIR = Path("logs")
|
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
LOG_FILE = LOG_DIR / f"qwen_vl_reasoning_test_{int(time.time())}.json"
|
|
||||||
|
|
||||||
|
|
||||||
def log(data: Dict[str, Any]):
|
|
||||||
"""追加写入调试日志。"""
|
|
||||||
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(data, ensure_ascii=False, indent=2))
|
|
||||||
f.write("\n\n")
|
|
||||||
|
|
||||||
|
|
||||||
def build_headers():
|
|
||||||
return {
|
|
||||||
"Authorization": f"Bearer {API_KEY}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def weather_tool_result(city: str = "上海", date: str = "今天"):
|
|
||||||
"""模拟天气工具返回固定结构。"""
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"city": city,
|
|
||||||
"date": date,
|
|
||||||
"forecast": "多云转晴,15~22℃,东风3级",
|
|
||||||
"source": "local-mock"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if not API_KEY:
|
|
||||||
raise SystemExit("未配置 API_KEY_QWEN 或 DASHSCOPE_API_KEY,无法测试")
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": "你是测试助手。"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": (
|
|
||||||
"请你想一个明确的8位数字,随便一个就行,只在思考过程中呈现,不要在最终回复里透露;"
|
|
||||||
"先调用天气查询工具;工具完成后再正常回答并把刚才思考出的数字告诉我。"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
tools = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "query_weather",
|
|
||||||
"description": "查询指定城市在指定日期的天气",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"city": {"type": "string", "description": "城市名称,如上海"},
|
|
||||||
"date": {"type": "string", "description": "日期,如今天/明天/2025-01-01"}
|
|
||||||
},
|
|
||||||
"required": ["city"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# ---- 第一次调用 ----
|
|
||||||
payload1 = {
|
|
||||||
"model": MODEL_ID,
|
|
||||||
"messages": messages,
|
|
||||||
"tools": tools,
|
|
||||||
"tool_choice": "auto",
|
|
||||||
"stream": False,
|
|
||||||
"max_tokens": 512,
|
|
||||||
# 关键:开启思考模式
|
|
||||||
"enable_thinking": True
|
|
||||||
}
|
|
||||||
|
|
||||||
with httpx.Client(http2=True, timeout=120) as client:
|
|
||||||
resp1 = client.post(f"{API_BASE}/chat/completions", json=payload1, headers=build_headers())
|
|
||||||
resp1.raise_for_status()
|
|
||||||
data1 = resp1.json()
|
|
||||||
log({"step": "call1_response", "raw": data1})
|
|
||||||
|
|
||||||
choice1 = data1["choices"][0]["message"]
|
|
||||||
tool_calls = choice1.get("tool_calls") or []
|
|
||||||
reasoning1 = choice1.get("reasoning_content")
|
|
||||||
|
|
||||||
print("第一次返回 reasoning_content 长度:", len(reasoning1 or ""))
|
|
||||||
print("第一次返回 tool_calls 数量:", len(tool_calls))
|
|
||||||
|
|
||||||
if not tool_calls:
|
|
||||||
print("模型未调用工具,测试无法继续。")
|
|
||||||
log({"error": "no_tool_calls"})
|
|
||||||
return
|
|
||||||
|
|
||||||
# 只取第一个工具调用
|
|
||||||
tc = tool_calls[0]
|
|
||||||
args_json = tc["function"].get("arguments") or "{}"
|
|
||||||
try:
|
|
||||||
args = json.loads(args_json)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
args = {}
|
|
||||||
tool_result = weather_tool_result(
|
|
||||||
city=args.get("city", "上海"),
|
|
||||||
date=args.get("date", "今天")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 构建第二轮消息:包含第一次assistant消息和tool结果
|
|
||||||
messages2 = messages + [
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": choice1.get("content", ""),
|
|
||||||
"tool_calls": tool_calls,
|
|
||||||
# 保留 reasoning_content 原样,核心验证点
|
|
||||||
"reasoning_content": reasoning1 or ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": tc.get("id"),
|
|
||||||
"name": tc["function"]["name"],
|
|
||||||
"content": json.dumps(tool_result, ensure_ascii=False)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
payload2 = {
|
|
||||||
"model": MODEL_ID,
|
|
||||||
"messages": messages2,
|
|
||||||
"tools": tools,
|
|
||||||
"tool_choice": "auto",
|
|
||||||
"stream": False,
|
|
||||||
"max_tokens": 512,
|
|
||||||
# 同步思考模式
|
|
||||||
"enable_thinking": True
|
|
||||||
}
|
|
||||||
|
|
||||||
with httpx.Client(http2=True, timeout=120) as client:
|
|
||||||
resp2 = client.post(f"{API_BASE}/chat/completions", json=payload2, headers=build_headers())
|
|
||||||
resp2.raise_for_status()
|
|
||||||
data2 = resp2.json()
|
|
||||||
log({"step": "call2_response", "raw": data2, "messages_sent": messages2})
|
|
||||||
|
|
||||||
choice2 = data2["choices"][0]["message"]
|
|
||||||
reasoning2 = choice2.get("reasoning_content")
|
|
||||||
content2 = choice2.get("content")
|
|
||||||
|
|
||||||
print("第二次返回 reasoning_content 长度:", len(reasoning2 or ""))
|
|
||||||
print("第二次最终回复:", content2)
|
|
||||||
print(f"完整日志已保存到: {LOG_FILE}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
33
scripts/cleanup_misplaced_web.sh
Normal file
33
scripts/cleanup_misplaced_web.sh
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 清理误迁到 ~/.agents/web 的运行态数据副本。
|
||||||
|
#
|
||||||
|
# 背景:首次迁移时脚本未加载 .env、误判为 web 模式,把数据复制到了
|
||||||
|
# ~/.agents/web/。源数据仍在源码树,web 只是程序读不到的错位副本。
|
||||||
|
# 本机终端有完整权限,可安全删除。
|
||||||
|
#
|
||||||
|
# 用法:bash scripts/cleanup_misplaced_web.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WEB_DIR="${HOME}/.agents/web"
|
||||||
|
|
||||||
|
if [ ! -e "$WEB_DIR" ]; then
|
||||||
|
echo "目标不存在,无需清理:$WEB_DIR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "将删除错位副本:$WEB_DIR"
|
||||||
|
du -sh "$WEB_DIR" 2>/dev/null || true
|
||||||
|
printf "确认删除?[y/N] "
|
||||||
|
read -r ans
|
||||||
|
case "$ans" in
|
||||||
|
y|Y|yes|YES)
|
||||||
|
# 先清掉可能存在的不可变标志(docx skill 等附带的 uchg/schg),再删除。
|
||||||
|
chflags -R nouchg,noschg "$WEB_DIR" 2>/dev/null || true
|
||||||
|
rm -rf "$WEB_DIR"
|
||||||
|
echo "已删除 $WEB_DIR"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "已取消,未删除任何文件。"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
166
scripts/migrate_runtime_data.py
Normal file
166
scripts/migrate_runtime_data.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""运行态数据迁移脚本:源码树 -> 运行态根目录(~/.agents/<mode>)。
|
||||||
|
|
||||||
|
把历史遗留在源码树内的运行态数据迁移到新的运行态根目录。目标位置直接复用
|
||||||
|
``config`` 模块的解析结果(``config.DATA_DIR`` / ``USER_SPACE_DIR`` / ... ),
|
||||||
|
因此与程序运行时使用的是**同一套路径**——包括 ``.env`` 中的
|
||||||
|
``TERMINAL_SANDBOX_MODE`` 与各路径环境变量。这样可避免脚本自行推断模式与
|
||||||
|
程序实际读取位置不一致的问题。
|
||||||
|
|
||||||
|
策略(与既定决策一致):
|
||||||
|
|
||||||
|
- logs/ 跳过不迁(历史日志直接丢弃)。
|
||||||
|
- data / users / api / sub_agent/tasks
|
||||||
|
先打包备份到 ``<运行态根上级>/backup_<时间戳>.tar.gz``,
|
||||||
|
校验备份成功后再复制到目标位置;复制采用「不覆盖已存在目标」的
|
||||||
|
保守策略,全程复制而非移动,旧目录保留,便于回滚。
|
||||||
|
|
||||||
|
本脚本只做「复制 + 备份」,不删除任何源数据,确保完全可回滚。
|
||||||
|
确认新目录工作正常后,可自行手动删除源码树内的旧目录。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/migrate_runtime_data.py # 实际执行(按 .env 决定 host/web)
|
||||||
|
python scripts/migrate_runtime_data.py --dry-run # 仅打印将要做什么
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 确保以 `python scripts/migrate_runtime_data.py` 直接运行时也能导入仓库根的 config 包。
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(REPO_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
# 需要迁移的运行态目录:源码树相对路径 -> 目标绝对路径(取自 config,与程序一致)。
|
||||||
|
# logs 不在其列:历史日志直接丢弃。
|
||||||
|
# sub_agent/tasks 旧默认在 ./sub_agent/tasks,新默认跟随 DATA_DIR -> <DATA_DIR>/sub_agent_tasks。
|
||||||
|
def _migrate_targets() -> dict[str, Path]:
|
||||||
|
return {
|
||||||
|
"data": Path(config.DATA_DIR),
|
||||||
|
"users": Path(config.USER_SPACE_DIR),
|
||||||
|
"api": Path(config.API_USER_SPACE_DIR).parent, # API_USER_SPACE_DIR=<root>/api/users,迁整个 api/
|
||||||
|
"sub_agent/tasks": Path(config.SUB_AGENT_TASKS_BASE_DIR),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _human_size(num: int) -> str:
|
||||||
|
value = float(num)
|
||||||
|
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||||
|
if value < 1024 or unit == "TB":
|
||||||
|
return f"{value:.1f}{unit}"
|
||||||
|
value /= 1024
|
||||||
|
return f"{num}B"
|
||||||
|
|
||||||
|
|
||||||
|
def _dir_size(path: Path) -> int:
|
||||||
|
total = 0
|
||||||
|
for p in path.rglob("*"):
|
||||||
|
if p.is_file():
|
||||||
|
try:
|
||||||
|
total += p.stat().st_size
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def _backup(sources: list[Path], backup_root: Path, *, dry_run: bool) -> Path | None:
|
||||||
|
if not sources:
|
||||||
|
return None
|
||||||
|
backup_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
archive = backup_root / f"backup_{stamp}.tar.gz"
|
||||||
|
print(f"[backup] 打包 {len(sources)} 个目录 -> {archive}")
|
||||||
|
if dry_run:
|
||||||
|
return archive
|
||||||
|
with tarfile.open(archive, "w:gz") as tar:
|
||||||
|
for src in sources:
|
||||||
|
# 用相对仓库根的路径作为归档内路径,保留 sub_agent/tasks 这类多级结构,避免重名碰撞。
|
||||||
|
try:
|
||||||
|
arcname = str(src.relative_to(REPO_ROOT))
|
||||||
|
except ValueError:
|
||||||
|
arcname = src.name
|
||||||
|
tar.add(src, arcname=arcname)
|
||||||
|
# 校验:备份文件存在且非空,且能成功列出成员
|
||||||
|
if not archive.exists() or archive.stat().st_size == 0:
|
||||||
|
raise RuntimeError(f"备份文件异常(不存在或为空): {archive}")
|
||||||
|
with tarfile.open(archive, "r:gz") as tar:
|
||||||
|
members = tar.getnames()
|
||||||
|
if not members:
|
||||||
|
raise RuntimeError(f"备份内容为空,已中止以保护数据: {archive}")
|
||||||
|
print(f"[backup] 备份完成并校验通过:{_human_size(archive.stat().st_size)},成员 {len(members)} 项")
|
||||||
|
return archive
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_tree(src: Path, dst: Path, *, dry_run: bool) -> None:
|
||||||
|
if dst.exists():
|
||||||
|
print(f"[skip] 目标已存在,跳过以避免覆盖:{dst}")
|
||||||
|
return
|
||||||
|
print(f"[copy] {src} -> {dst} ({_human_size(_dir_size(src))})")
|
||||||
|
if dry_run:
|
||||||
|
return
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copytree(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="迁移运行态数据到运行态根目录(~/.agents/<mode>)")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不实际写入")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
targets = _migrate_targets()
|
||||||
|
runtime_root = Path(config.RUNTIME_ROOT)
|
||||||
|
print(f"运行态根目录(取自 config,与程序一致):{runtime_root}")
|
||||||
|
print(f"源码树根目录:{REPO_ROOT}")
|
||||||
|
print("注意:logs/ 不迁移(历史日志按既定决策直接丢弃)")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
# 收集存在且非空的源目录
|
||||||
|
present: list[tuple[Path, Path]] = []
|
||||||
|
for src_rel, dst in targets.items():
|
||||||
|
src = REPO_ROOT / src_rel
|
||||||
|
if src.is_dir() and any(src.iterdir()):
|
||||||
|
present.append((src, dst))
|
||||||
|
else:
|
||||||
|
print(f"[absent] 源目录不存在或为空,跳过:{src}")
|
||||||
|
|
||||||
|
if not present:
|
||||||
|
print("没有需要迁移的运行态数据。")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 1) 备份(备份到运行态根的上级目录,与运行态根同级)
|
||||||
|
backup_root = runtime_root.parent
|
||||||
|
try:
|
||||||
|
_backup([s for s, _ in present], backup_root, dry_run=args.dry_run)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[error] 备份失败,已中止迁移(未改动任何数据):{exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 2) 复制(不覆盖已有目标,源目录保留)
|
||||||
|
for src, dst in present:
|
||||||
|
try:
|
||||||
|
_copy_tree(src, dst, dry_run=args.dry_run)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[error] 复制失败:{src} -> {dst}:{exc}", file=sys.stderr)
|
||||||
|
print(" 已迁移的部分保留,源数据未删除,可重跑或手动处理。", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("-" * 60)
|
||||||
|
if args.dry_run:
|
||||||
|
print("dry-run 完成:以上为将要执行的操作,未实际写入。")
|
||||||
|
else:
|
||||||
|
print("迁移完成。源码树内的旧目录已保留,确认新目录工作正常后可手动删除。")
|
||||||
|
print(f"备份位置:{backup_root}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from config import LOGS_DIR
|
from config import LOGS_DIR
|
||||||
|
from utils.log_rotation import append_line as _append_rotating
|
||||||
|
|
||||||
# 文件名安全处理
|
# 文件名安全处理
|
||||||
|
|
||||||
@ -148,10 +149,8 @@ CONN_DIAG_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "conn_diag.log"
|
|||||||
|
|
||||||
|
|
||||||
def _write_log(file_path: Path, message: str) -> None:
|
def _write_log(file_path: Path, message: str) -> None:
|
||||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
||||||
with file_path.open('a', encoding='utf-8') as f:
|
_append_rotating(file_path, f"[{timestamp}] {message}")
|
||||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
|
||||||
f.write(f"[{timestamp}] {message}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def debug_log(message):
|
def debug_log(message):
|
||||||
|
|||||||
@ -18,17 +18,28 @@ class ConfigPathsResolutionTest(unittest.TestCase):
|
|||||||
import json
|
import json
|
||||||
import config
|
import config
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
|
"RUNTIME_ROOT": config.RUNTIME_ROOT,
|
||||||
"DEFAULT_PROJECT_PATH": config.DEFAULT_PROJECT_PATH,
|
"DEFAULT_PROJECT_PATH": config.DEFAULT_PROJECT_PATH,
|
||||||
"HOST_WORKSPACES_FILE": config.HOST_WORKSPACES_FILE,
|
"HOST_WORKSPACES_FILE": config.HOST_WORKSPACES_FILE,
|
||||||
|
"PROMPTS_DIR": config.PROMPTS_DIR,
|
||||||
"DATA_DIR": config.DATA_DIR,
|
"DATA_DIR": config.DATA_DIR,
|
||||||
"LOGS_DIR": config.LOGS_DIR,
|
"LOGS_DIR": config.LOGS_DIR,
|
||||||
"USER_SPACE_DIR": config.USER_SPACE_DIR,
|
"USER_SPACE_DIR": config.USER_SPACE_DIR,
|
||||||
"API_USER_SPACE_DIR": config.API_USER_SPACE_DIR,
|
"API_USER_SPACE_DIR": config.API_USER_SPACE_DIR,
|
||||||
"SUB_AGENT_STATE_FILE": config.SUB_AGENT_STATE_FILE,
|
"SUB_AGENT_STATE_FILE": config.SUB_AGENT_STATE_FILE,
|
||||||
|
"SUB_AGENT_TASKS_BASE_DIR": config.SUB_AGENT_TASKS_BASE_DIR,
|
||||||
}, ensure_ascii=False))
|
}, ensure_ascii=False))
|
||||||
"""
|
"""
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
env["PYTHONPATH"] = str(self.repo_root)
|
env["PYTHONPATH"] = str(self.repo_root)
|
||||||
|
# 清掉可能从外部 .env / 环境继承的覆盖项,保证用例可控。
|
||||||
|
for key in (
|
||||||
|
"TERMINAL_SANDBOX_MODE", "AGENTS_HOST_HOME", "AGENTS_WEB_HOME",
|
||||||
|
"DATA_DIR", "LOGS_DIR", "USER_SPACE_DIR", "API_USER_SPACE_DIR",
|
||||||
|
"DEFAULT_PROJECT_PATH", "HOST_WORKSPACES_FILE", "PROMPTS_DIR",
|
||||||
|
"SUB_AGENT_STATE_FILE", "SUB_AGENT_TASKS_BASE_DIR",
|
||||||
|
):
|
||||||
|
env.pop(key, None)
|
||||||
if extra_env:
|
if extra_env:
|
||||||
env.update(extra_env)
|
env.update(extra_env)
|
||||||
completed = subprocess.run(
|
completed = subprocess.run(
|
||||||
@ -43,36 +54,98 @@ print(json.dumps({
|
|||||||
self.assertTrue(lines, f"stdout is empty, stderr={completed.stderr}")
|
self.assertTrue(lines, f"stdout is empty, stderr={completed.stderr}")
|
||||||
return json.loads(lines[-1])
|
return json.loads(lines[-1])
|
||||||
|
|
||||||
def test_default_paths_are_resolved_from_repo_root(self):
|
def test_runtime_dirs_default_to_web_home(self):
|
||||||
|
"""默认(非 host)模式下,运行态目录锚定到 ~/.agents/web。"""
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
data = self._load_paths(cwd=Path(td))
|
data = self._load_paths(cwd=Path(td), extra_env={"TERMINAL_SANDBOX_MODE": "docker"})
|
||||||
|
|
||||||
for key, raw in data.items():
|
web_home = Path("~/.agents/web").expanduser().resolve()
|
||||||
value = Path(str(raw))
|
self.assertEqual(data["RUNTIME_ROOT"], str(web_home))
|
||||||
self.assertTrue(value.is_absolute(), f"{key} is not absolute: {raw}")
|
self.assertEqual(data["DATA_DIR"], str(web_home / "data"))
|
||||||
|
self.assertEqual(data["LOGS_DIR"], str(web_home / "logs"))
|
||||||
|
self.assertEqual(data["USER_SPACE_DIR"], str(web_home / "users"))
|
||||||
|
self.assertEqual(data["API_USER_SPACE_DIR"], str(web_home / "api" / "users"))
|
||||||
|
# 数据文件跟随 DATA_DIR
|
||||||
|
self.assertEqual(data["SUB_AGENT_STATE_FILE"], str(web_home / "data" / "sub_agents.json"))
|
||||||
|
self.assertEqual(data["SUB_AGENT_TASKS_BASE_DIR"], str(web_home / "data" / "sub_agent_tasks"))
|
||||||
|
|
||||||
|
def test_runtime_dirs_switch_to_host_home(self):
|
||||||
|
"""host 模式下,运行态目录锚定到 ~/.agents/host。"""
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
data = self._load_paths(cwd=Path(td), extra_env={"TERMINAL_SANDBOX_MODE": "host"})
|
||||||
|
|
||||||
|
host_home = Path("~/.agents/host").expanduser().resolve()
|
||||||
|
self.assertEqual(data["RUNTIME_ROOT"], str(host_home))
|
||||||
|
self.assertEqual(data["DATA_DIR"], str(host_home / "data"))
|
||||||
|
self.assertEqual(data["LOGS_DIR"], str(host_home / "logs"))
|
||||||
|
|
||||||
|
def test_source_tree_paths_stay_anchored_to_repo_root(self):
|
||||||
|
"""配置类路径(提示词、host_workspaces.json、默认工作区)仍锚定源码树。"""
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
data = self._load_paths(cwd=Path(td), extra_env={"TERMINAL_SANDBOX_MODE": "docker"})
|
||||||
|
|
||||||
|
for key in ("DEFAULT_PROJECT_PATH", "HOST_WORKSPACES_FILE", "PROMPTS_DIR"):
|
||||||
|
value = Path(data[key])
|
||||||
|
self.assertTrue(value.is_absolute(), f"{key} not absolute: {data[key]}")
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
str(value).startswith(str(self.repo_root)),
|
str(value).startswith(str(self.repo_root)),
|
||||||
f"{key} should be anchored to repo root: {raw}",
|
f"{key} should anchor to repo root: {data[key]}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_relative_env_overrides_also_anchor_to_repo_root(self):
|
def test_mode_home_env_overrides_root(self):
|
||||||
|
"""模式根变量(AGENTS_HOST_HOME / AGENTS_WEB_HOME)整体搬迁运行态目录。"""
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
custom = Path(td) / "host_root"
|
||||||
|
data = self._load_paths(
|
||||||
|
cwd=Path(td),
|
||||||
|
extra_env={
|
||||||
|
"TERMINAL_SANDBOX_MODE": "host",
|
||||||
|
"AGENTS_HOST_HOME": str(custom),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(data["RUNTIME_ROOT"], str(custom.resolve()))
|
||||||
|
self.assertEqual(data["DATA_DIR"], str((custom / "data").resolve()))
|
||||||
|
self.assertEqual(data["LOGS_DIR"], str((custom / "logs").resolve()))
|
||||||
|
|
||||||
|
def test_specific_dir_env_has_highest_priority(self):
|
||||||
|
"""具体目录变量优先级高于模式根变量。"""
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
root = Path(td) / "root"
|
||||||
|
logs = Path(td) / "separate_logs"
|
||||||
|
data = self._load_paths(
|
||||||
|
cwd=Path(td),
|
||||||
|
extra_env={
|
||||||
|
"TERMINAL_SANDBOX_MODE": "web",
|
||||||
|
"AGENTS_WEB_HOME": str(root),
|
||||||
|
"LOGS_DIR": str(logs),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# DATA 跟随根变量,LOGS 被单独覆盖
|
||||||
|
self.assertEqual(data["DATA_DIR"], str((root / "data").resolve()))
|
||||||
|
self.assertEqual(data["LOGS_DIR"], str(logs.resolve()))
|
||||||
|
|
||||||
|
def test_relative_specific_dir_env_anchors_to_repo_root(self):
|
||||||
|
"""具体目录变量给相对路径时,仍锚定仓库根目录。"""
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
data = self._load_paths(
|
data = self._load_paths(
|
||||||
cwd=Path(td),
|
cwd=Path(td),
|
||||||
extra_env={
|
extra_env={
|
||||||
"DEFAULT_PROJECT_PATH": "./workspace_rel",
|
"TERMINAL_SANDBOX_MODE": "docker",
|
||||||
"DATA_DIR": "./data_rel",
|
"DATA_DIR": "./data_rel",
|
||||||
"LOGS_DIR": "./logs_rel",
|
"LOGS_DIR": "./logs_rel",
|
||||||
"USER_SPACE_DIR": "./users_rel",
|
"USER_SPACE_DIR": "./users_rel",
|
||||||
|
"DEFAULT_PROJECT_PATH": "./workspace_rel",
|
||||||
"HOST_WORKSPACES_FILE": "./config/host_workspaces_rel.json",
|
"HOST_WORKSPACES_FILE": "./config/host_workspaces_rel.json",
|
||||||
"SUB_AGENT_STATE_FILE": "./data/sub_agents_rel.json",
|
"SUB_AGENT_STATE_FILE": "./data/sub_agents_rel.json",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(data["DEFAULT_PROJECT_PATH"], str((self.repo_root / "workspace_rel").resolve()))
|
|
||||||
self.assertEqual(data["DATA_DIR"], str((self.repo_root / "data_rel").resolve()))
|
self.assertEqual(data["DATA_DIR"], str((self.repo_root / "data_rel").resolve()))
|
||||||
self.assertEqual(data["LOGS_DIR"], str((self.repo_root / "logs_rel").resolve()))
|
self.assertEqual(data["LOGS_DIR"], str((self.repo_root / "logs_rel").resolve()))
|
||||||
self.assertEqual(data["USER_SPACE_DIR"], str((self.repo_root / "users_rel").resolve()))
|
self.assertEqual(data["USER_SPACE_DIR"], str((self.repo_root / "users_rel").resolve()))
|
||||||
|
self.assertEqual(data["DEFAULT_PROJECT_PATH"], str((self.repo_root / "workspace_rel").resolve()))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
data["HOST_WORKSPACES_FILE"],
|
data["HOST_WORKSPACES_FILE"],
|
||||||
str((self.repo_root / "config" / "host_workspaces_rel.json").resolve()),
|
str((self.repo_root / "config" / "host_workspaces_rel.json").resolve()),
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""测试在 user 消息后插入 system 消息是否会被转换"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# 加载环境变量
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# 获取 Kimi 官方 API 配置(请通过环境变量提供)
|
|
||||||
api_key = os.getenv("MOONSHOT_API_KEY", "")
|
|
||||||
# 使用 Kimi 官方 API
|
|
||||||
api_base = os.getenv("MOONSHOT_API_BASE", "https://api.moonshot.cn/v1")
|
|
||||||
|
|
||||||
if not api_key:
|
|
||||||
raise SystemExit("请先设置环境变量 MOONSHOT_API_KEY")
|
|
||||||
|
|
||||||
# 构建测试消息
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": "你是一个AI助手。"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "请帮我看看我这句话后面的那条testcode消息的角色是system还是user"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": "testcode=11299"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("测试:在 user 消息后插入 system 消息")
|
|
||||||
print("使用 Kimi 官方 API (k2.5)")
|
|
||||||
print("=" * 60)
|
|
||||||
print("\n发送的消息序列:")
|
|
||||||
for i, msg in enumerate(messages):
|
|
||||||
print(f"{i+1}. role={msg['role']}, content={msg['content'][:50]}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("调用 Kimi API...")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# 调用 API
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"model": "kimi-k2.5",
|
|
||||||
"messages": messages
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{api_base}/chat/completions",
|
|
||||||
headers=headers,
|
|
||||||
json=payload,
|
|
||||||
timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"\nHTTP 状态码: {response.status_code}")
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
result = response.json()
|
|
||||||
content = result["choices"][0]["message"]["content"]
|
|
||||||
print("\n模型回复:")
|
|
||||||
print("-" * 60)
|
|
||||||
print(content)
|
|
||||||
print("-" * 60)
|
|
||||||
else:
|
|
||||||
print(f"\n错误响应: {response.text}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n请求失败: {e}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("测试完成")
|
|
||||||
print("=" * 60)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
@ -21,7 +21,8 @@ try:
|
|||||||
DEFAULT_RESPONSE_MAX_TOKENS,
|
DEFAULT_RESPONSE_MAX_TOKENS,
|
||||||
THINKING_API_BASE_URL,
|
THINKING_API_BASE_URL,
|
||||||
THINKING_API_KEY,
|
THINKING_API_KEY,
|
||||||
THINKING_MODEL_ID
|
THINKING_MODEL_ID,
|
||||||
|
LOGS_DIR,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import sys
|
import sys
|
||||||
@ -37,9 +38,17 @@ except ImportError:
|
|||||||
DEFAULT_RESPONSE_MAX_TOKENS,
|
DEFAULT_RESPONSE_MAX_TOKENS,
|
||||||
THINKING_API_BASE_URL,
|
THINKING_API_BASE_URL,
|
||||||
THINKING_API_KEY,
|
THINKING_API_KEY,
|
||||||
THINKING_MODEL_ID
|
THINKING_MODEL_ID,
|
||||||
|
LOGS_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from utils.log_rotation import append_line, prune_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _api_dump_enabled() -> bool:
|
||||||
|
"""API 请求体落盘开关,默认关闭。"""
|
||||||
|
return str(os.environ.get("AGENT_API_DUMP_ENABLED", "")).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
||||||
def __init__(self, thinking_mode: bool = True, web_mode: bool = False):
|
def __init__(self, thinking_mode: bool = True, web_mode: bool = False):
|
||||||
self.fast_api_config = {
|
self.fast_api_config = {
|
||||||
@ -79,20 +88,19 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
self.last_call_used_thinking = False # 最近一次调用是否使用思考模型
|
self.last_call_used_thinking = False # 最近一次调用是否使用思考模型
|
||||||
# 最近一次API错误详情
|
# 最近一次API错误详情
|
||||||
self.last_error_info: Optional[Dict[str, Any]] = None
|
self.last_error_info: Optional[Dict[str, Any]] = None
|
||||||
# 请求体落盘目录
|
# 请求体落盘目录(跟随 LOGS_DIR,默认 ~/.agents/<mode>/logs/api_requests)
|
||||||
self.request_dump_dir = Path(__file__).resolve().parents[1] / "logs" / "api_requests"
|
self.request_dump_dir = Path(LOGS_DIR) / "api_requests"
|
||||||
self.request_dump_dir.mkdir(parents=True, exist_ok=True)
|
self.debug_log_path = Path(LOGS_DIR) / "api_debug.log"
|
||||||
self.debug_log_path = Path(__file__).resolve().parents[1] / "logs" / "api_debug.log"
|
|
||||||
|
|
||||||
def _debug_log(self, payload: Dict[str, Any]) -> None:
|
def _debug_log(self, payload: Dict[str, Any]) -> None:
|
||||||
|
if not _api_dump_enabled():
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
entry = {
|
entry = {
|
||||||
"ts": datetime.now().isoformat(),
|
"ts": datetime.now().isoformat(),
|
||||||
**payload
|
**payload
|
||||||
}
|
}
|
||||||
self.debug_log_path.parent.mkdir(parents=True, exist_ok=True)
|
append_line(self.debug_log_path, json.dumps(entry, ensure_ascii=False))
|
||||||
with self.debug_log_path.open("a", encoding="utf-8") as f:
|
|
||||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -482,15 +490,18 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"model_id": config.get("model_id") or fallback["model_id"]
|
"model_id": config.get("model_id") or fallback["model_id"]
|
||||||
}
|
}
|
||||||
|
|
||||||
def _dump_request_payload(self, payload: Dict, api_config: Dict, headers: Dict) -> Path:
|
def _dump_request_payload(self, payload: Dict, api_config: Dict, headers: Dict) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
将本次请求的payload、headers、配置落盘,便于排查400等错误。
|
将本次请求的payload、headers、配置落盘,便于排查400等错误。
|
||||||
返回写入的文件路径。
|
返回写入的文件路径;落盘开关关闭(默认)时返回 None。
|
||||||
"""
|
"""
|
||||||
|
if not _api_dump_enabled():
|
||||||
|
return None
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
filename = f"req_{timestamp}.json"
|
filename = f"req_{timestamp}.json"
|
||||||
path = self.request_dump_dir / filename
|
path = self.request_dump_dir / filename
|
||||||
try:
|
try:
|
||||||
|
self.request_dump_dir.mkdir(parents=True, exist_ok=True)
|
||||||
headers_sanitized = {}
|
headers_sanitized = {}
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
headers_sanitized[k] = "***" if k.lower() == "authorization" else v
|
headers_sanitized[k] = "***" if k.lower() == "authorization" else v
|
||||||
@ -501,6 +512,8 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"payload": payload
|
"payload": payload
|
||||||
}
|
}
|
||||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
# 按份保留:只留最近 N 个请求落盘文件
|
||||||
|
prune_dir(self.request_dump_dir, pattern="req_*.json")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._print(f"{OUTPUT_FORMATS['warning']} 请求体落盘失败: {exc}")
|
self._print(f"{OUTPUT_FORMATS['warning']} 请求体落盘失败: {exc}")
|
||||||
return path
|
return path
|
||||||
@ -783,7 +796,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"error_text": error_text,
|
"error_text": error_text,
|
||||||
"error_type": None,
|
"error_type": None,
|
||||||
"error_message": None,
|
"error_message": None,
|
||||||
"request_dump": str(dump_path),
|
"request_dump": (str(dump_path) if dump_path else None),
|
||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key
|
"model_key": self.model_key
|
||||||
@ -803,7 +816,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key,
|
"model_key": self.model_key,
|
||||||
"request_dump": str(dump_path)
|
"request_dump": (str(dump_path) if dump_path else None)
|
||||||
})
|
})
|
||||||
self._print(
|
self._print(
|
||||||
f"{OUTPUT_FORMATS['error']} API请求失败 ({response.status_code}): {error_text} "
|
f"{OUTPUT_FORMATS['error']} API请求失败 ({response.status_code}): {error_text} "
|
||||||
@ -837,7 +850,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"error_text": error_text,
|
"error_text": error_text,
|
||||||
"error_type": None,
|
"error_type": None,
|
||||||
"error_message": None,
|
"error_message": None,
|
||||||
"request_dump": str(dump_path),
|
"request_dump": (str(dump_path) if dump_path else None),
|
||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key
|
"model_key": self.model_key
|
||||||
@ -857,7 +870,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key,
|
"model_key": self.model_key,
|
||||||
"request_dump": str(dump_path)
|
"request_dump": (str(dump_path) if dump_path else None)
|
||||||
})
|
})
|
||||||
self._print(
|
self._print(
|
||||||
f"{OUTPUT_FORMATS['error']} API请求失败 ({response.status_code}): {error_text} "
|
f"{OUTPUT_FORMATS['error']} API请求失败 ({response.status_code}): {error_text} "
|
||||||
@ -882,7 +895,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"error_type": "connection_error",
|
"error_type": "connection_error",
|
||||||
"error_message": f"无法连接到API服务器: {connect_detail}",
|
"error_message": f"无法连接到API服务器: {connect_detail}",
|
||||||
"error_detail": connect_detail,
|
"error_detail": connect_detail,
|
||||||
"request_dump": str(dump_path),
|
"request_dump": (str(dump_path) if dump_path else None),
|
||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key
|
"model_key": self.model_key
|
||||||
@ -895,7 +908,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key,
|
"model_key": self.model_key,
|
||||||
"request_dump": str(dump_path)
|
"request_dump": (str(dump_path) if dump_path else None)
|
||||||
})
|
})
|
||||||
self._mark_request_error(dump_path, error_text=f"connect_error: {connect_detail}")
|
self._mark_request_error(dump_path, error_text=f"connect_error: {connect_detail}")
|
||||||
yield {"error": self.last_error_info}
|
yield {"error": self.last_error_info}
|
||||||
@ -906,7 +919,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"error_text": "timeout",
|
"error_text": "timeout",
|
||||||
"error_type": "timeout",
|
"error_type": "timeout",
|
||||||
"error_message": "API请求超时",
|
"error_message": "API请求超时",
|
||||||
"request_dump": str(dump_path),
|
"request_dump": (str(dump_path) if dump_path else None),
|
||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key
|
"model_key": self.model_key
|
||||||
@ -918,7 +931,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key,
|
"model_key": self.model_key,
|
||||||
"request_dump": str(dump_path)
|
"request_dump": (str(dump_path) if dump_path else None)
|
||||||
})
|
})
|
||||||
self._mark_request_error(dump_path, error_text="timeout")
|
self._mark_request_error(dump_path, error_text="timeout")
|
||||||
yield {"error": self.last_error_info}
|
yield {"error": self.last_error_info}
|
||||||
@ -930,7 +943,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"error_text": error_text,
|
"error_text": error_text,
|
||||||
"error_type": "exception",
|
"error_type": "exception",
|
||||||
"error_message": error_text,
|
"error_message": error_text,
|
||||||
"request_dump": str(dump_path),
|
"request_dump": (str(dump_path) if dump_path else None),
|
||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key
|
"model_key": self.model_key
|
||||||
@ -942,7 +955,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
|
|||||||
"base_url": api_config.get("base_url"),
|
"base_url": api_config.get("base_url"),
|
||||||
"model_id": api_config.get("model_id"),
|
"model_id": api_config.get("model_id"),
|
||||||
"model_key": self.model_key,
|
"model_key": self.model_key,
|
||||||
"request_dump": str(dump_path)
|
"request_dump": (str(dump_path) if dump_path else None)
|
||||||
})
|
})
|
||||||
self._mark_request_error(dump_path, error_text=error_text)
|
self._mark_request_error(dump_path, error_text=error_text)
|
||||||
yield {"error": self.last_error_info}
|
yield {"error": self.last_error_info}
|
||||||
|
|||||||
@ -3,30 +3,25 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import threading
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from config import LOGS_DIR
|
from config import LOGS_DIR
|
||||||
|
from utils.log_rotation import append_line
|
||||||
|
|
||||||
_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "host_workspace_debug.log"
|
_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "host_workspace_debug.log"
|
||||||
_LOCK = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def write_host_workspace_debug(event: str, **payload: Any) -> None:
|
def write_host_workspace_debug(event: str, **payload: Any) -> None:
|
||||||
"""写入一条宿主机工作区调试日志。"""
|
"""写入一条宿主机工作区调试日志(超过阈值自动轮转)。"""
|
||||||
try:
|
try:
|
||||||
record = {
|
record = {
|
||||||
"ts": datetime.now().isoformat(),
|
"ts": datetime.now().isoformat(),
|
||||||
"event": event,
|
"event": event,
|
||||||
**payload,
|
**payload,
|
||||||
}
|
}
|
||||||
line = json.dumps(record, ensure_ascii=False, default=str)
|
append_line(_LOG_FILE, json.dumps(record, ensure_ascii=False, default=str))
|
||||||
with _LOCK:
|
|
||||||
_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with _LOG_FILE.open("a", encoding="utf-8") as fp:
|
|
||||||
fp.write(line + "\n")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# 调试日志不应影响主流程
|
# 调试日志不应影响主流程
|
||||||
return
|
return
|
||||||
|
|||||||
127
utils/log_rotation.py
Normal file
127
utils/log_rotation.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""日志与落盘文件的轮转/清理工具。
|
||||||
|
|
||||||
|
提供两类策略:
|
||||||
|
|
||||||
|
1. ``RotatingFileHandler`` 风格的"按大小轮转"——用于持续追加的单文件日志
|
||||||
|
(如 host_workspace_debug.log、chunk_*.log)。封装为 :func:`append_line`,
|
||||||
|
单文件超过阈值即滚动,保留固定份数。
|
||||||
|
2. "按份数保留"——用于一请求一文件的落盘目录(如 api_requests/),写入新文件
|
||||||
|
后清理最旧的,只保留最近 N 个。
|
||||||
|
|
||||||
|
阈值可通过环境变量覆盖:
|
||||||
|
|
||||||
|
- ``AGENT_LOG_ROTATE_MAX_BYTES`` 单文件轮转阈值(字节,默认 20MB)
|
||||||
|
- ``AGENT_LOG_ROTATE_BACKUPS`` 追加型日志保留的轮转份数(默认 3)
|
||||||
|
- ``AGENT_DUMP_KEEP`` 按份计量目录保留的文件数(默认 30)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name: str, default: int) -> int:
|
||||||
|
try:
|
||||||
|
value = int(str(os.environ.get(name, "")).strip())
|
||||||
|
return value if value > 0 else default
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_MAX_BYTES = _env_int("AGENT_LOG_ROTATE_MAX_BYTES", 20 * 1024 * 1024) # 20MB
|
||||||
|
DEFAULT_BACKUPS = _env_int("AGENT_LOG_ROTATE_BACKUPS", 3)
|
||||||
|
DEFAULT_DUMP_KEEP = _env_int("AGENT_DUMP_KEEP", 30)
|
||||||
|
|
||||||
|
# 每个文件路径一把锁,避免并发写入与轮转竞争。
|
||||||
|
_locks: dict[str, threading.Lock] = {}
|
||||||
|
_locks_guard = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _lock_for(path: Path) -> threading.Lock:
|
||||||
|
key = str(path)
|
||||||
|
with _locks_guard:
|
||||||
|
lock = _locks.get(key)
|
||||||
|
if lock is None:
|
||||||
|
lock = threading.Lock()
|
||||||
|
_locks[key] = lock
|
||||||
|
return lock
|
||||||
|
|
||||||
|
|
||||||
|
def _rotate(path: Path, backups: int) -> None:
|
||||||
|
"""执行 ``foo.log`` -> ``foo.log.1`` -> ``foo.log.2`` ... 的滚动。"""
|
||||||
|
try:
|
||||||
|
# 删除最旧一份
|
||||||
|
oldest = path.with_name(f"{path.name}.{backups}")
|
||||||
|
if oldest.exists():
|
||||||
|
oldest.unlink()
|
||||||
|
# 依次后移:.(n-1) -> .n
|
||||||
|
for idx in range(backups - 1, 0, -1):
|
||||||
|
src = path.with_name(f"{path.name}.{idx}")
|
||||||
|
if src.exists():
|
||||||
|
src.rename(path.with_name(f"{path.name}.{idx + 1}"))
|
||||||
|
# 当前文件 -> .1
|
||||||
|
if path.exists():
|
||||||
|
path.rename(path.with_name(f"{path.name}.1"))
|
||||||
|
except Exception:
|
||||||
|
# 轮转失败不应阻断主流程,最坏情况是文件继续增长
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def append_line(
|
||||||
|
path: Path,
|
||||||
|
line: str,
|
||||||
|
*,
|
||||||
|
max_bytes: int = DEFAULT_MAX_BYTES,
|
||||||
|
backups: int = DEFAULT_BACKUPS,
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
) -> None:
|
||||||
|
"""向 ``path`` 追加一行(自动补换行),超过 ``max_bytes`` 时先轮转。
|
||||||
|
|
||||||
|
线程安全。任何异常都被吞掉,调试日志不应影响主流程。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
path = Path(path)
|
||||||
|
text = line if line.endswith("\n") else line + "\n"
|
||||||
|
with _lock_for(path):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
size = path.stat().st_size if path.exists() else 0
|
||||||
|
except OSError:
|
||||||
|
size = 0
|
||||||
|
if size and size + len(text.encode(encoding)) > max_bytes:
|
||||||
|
_rotate(path, backups)
|
||||||
|
with path.open("a", encoding=encoding) as fp:
|
||||||
|
fp.write(text)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def prune_dir(
|
||||||
|
directory: Path,
|
||||||
|
*,
|
||||||
|
pattern: str = "*",
|
||||||
|
keep: int = DEFAULT_DUMP_KEEP,
|
||||||
|
) -> None:
|
||||||
|
"""只保留 ``directory`` 下匹配 ``pattern`` 的最近 ``keep`` 个文件,删除其余最旧的。
|
||||||
|
|
||||||
|
线程安全。任何异常都被吞掉。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
directory = Path(directory)
|
||||||
|
if keep <= 0 or not directory.is_dir():
|
||||||
|
return
|
||||||
|
with _lock_for(directory):
|
||||||
|
files = [p for p in directory.glob(pattern) if p.is_file()]
|
||||||
|
if len(files) <= keep:
|
||||||
|
return
|
||||||
|
files.sort(key=lambda p: p.stat().st_mtime)
|
||||||
|
for stale in files[: len(files) - keep]:
|
||||||
|
try:
|
||||||
|
stale.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
try:
|
try:
|
||||||
from config import LOGS_DIR, LOG_LEVEL, LOG_FORMAT
|
from config import LOGS_DIR, LOG_LEVEL, LOG_FORMAT
|
||||||
@ -14,6 +15,8 @@ except ImportError:
|
|||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
from config import LOGS_DIR, LOG_LEVEL, LOG_FORMAT
|
from config import LOGS_DIR, LOG_LEVEL, LOG_FORMAT
|
||||||
|
|
||||||
|
from utils.log_rotation import DEFAULT_MAX_BYTES, DEFAULT_BACKUPS
|
||||||
|
|
||||||
def _env_flag(name: str, default: bool = False) -> bool:
|
def _env_flag(name: str, default: bool = False) -> bool:
|
||||||
value = str(os.environ.get(name, "")).strip().lower()
|
value = str(os.environ.get(name, "")).strip().lower()
|
||||||
if not value:
|
if not value:
|
||||||
@ -61,7 +64,12 @@ def setup_logger(name: str, log_file: str = None) -> logging.Logger:
|
|||||||
# 确保日志目录存在
|
# 确保日志目录存在
|
||||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
file_handler = logging.FileHandler(file_path, encoding='utf-8')
|
file_handler = RotatingFileHandler(
|
||||||
|
file_path,
|
||||||
|
maxBytes=DEFAULT_MAX_BYTES,
|
||||||
|
backupCount=DEFAULT_BACKUPS,
|
||||||
|
encoding='utf-8',
|
||||||
|
)
|
||||||
file_handler.setLevel(getattr(logging, LOG_LEVEL))
|
file_handler.setLevel(getattr(logging, LOG_LEVEL))
|
||||||
file_handler.setFormatter(formatter)
|
file_handler.setFormatter(formatter)
|
||||||
logger.addHandler(file_handler)
|
logger.addHandler(file_handler)
|
||||||
|
|||||||
18022
web_server.log
18022
web_server.log
File diff suppressed because it is too large
Load Diff
20
webapp.log
20
webapp.log
@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
==================================================
|
|
||||||
ð¤ AI Agent 系统启动
|
|
||||||
==================================================
|
|
||||||
✅ [成功] 已选择项目路径: /Users/jojo/Desktop/test
|
|
||||||
ℹ️ [信息] 运行模式: Web(默认)
|
|
||||||
ℹ️ [信息] 思考模式: 开启(默认)
|
|
||||||
|
|
||||||
✅ [成功] 正在启动Web服务器...
|
|
||||||
ℹ️ [信息] 项目路径: /Users/jojo/Desktop/test
|
|
||||||
[Init] 初始化Web系统...
|
|
||||||
[Init] 项目路径: /Users/jojo/Desktop/test
|
|
||||||
[Init] 运行模式: 思考模式(首次思考,后续快速)
|
|
||||||
[Init] 自动修复: 关闭
|
|
||||||
[Init] 调试日志: /Users/jojo/Desktop/agents/正在修复中/agents/logs/debug_stream.log
|
|
||||||
✅ [成功] Web系统初始化完成(多用户模式)
|
|
||||||
* Serving Flask app 'server.app_legacy'
|
|
||||||
* Debug mode: off
|
|
||||||
Address already in use
|
|
||||||
Port 8091 is in use by another program. Either identify and stop that program, or start the server with a different port.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
3120088
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
[0;1;32m●[0m webapp.service - Web Application Service
|
|
||||||
Loaded: loaded (]8;;file://iZ2ze5i9ndpg9j4i6l1sfdZ/etc/systemd/system/webapp.service/etc/systemd/system/webapp.service]8;;; [0;1;32menabled[0m; preset: [0;1;32menabled[0m)
|
|
||||||
Active: [0;1;32mactive (running)[0m since Fri 2025-10-31 18:39:09 CST; 609ms ago
|
|
||||||
Process: 392152 ExecStart=/opt/agent/start_webapp_new.sh (code=exited, status=0/SUCCESS)
|
|
||||||
Main PID: 392156 (python3)
|
|
||||||
Tasks: 1 (limit: 4200)
|
|
||||||
Memory: 41.6M (peak: 43.5M)
|
|
||||||
CPU: 504ms
|
|
||||||
CGroup: /system.slice/webapp.service
|
|
||||||
└─[0;38;5;245m392156 python3 web_server.py[0m
|
|
||||||
|
|
||||||
Oct 31 18:39:06 iZ2ze5i9ndpg9j4i6l1sfdZ systemd[1]: webapp.service: Scheduled restart job, restart counter is at 18.
|
|
||||||
Oct 31 18:39:06 iZ2ze5i9ndpg9j4i6l1sfdZ systemd[1]: Starting webapp.service - Web Application Service...
|
|
||||||
Oct 31 18:39:09 iZ2ze5i9ndpg9j4i6l1sfdZ systemd[1]: Started webapp.service - Web Application Service.
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
# 诡异Bug记录
|
|
||||||
|
|
||||||
- 诡异程度:★★★★☆
|
|
||||||
- 发现时间:2026-02-25 03:17:14
|
|
||||||
- 场景/模块:前端聊天流式渲染(思考块/工具块堆叠)
|
|
||||||
- 关联模型:minimax-m2.5
|
|
||||||
|
|
||||||
## 描述
|
|
||||||
使用 minimax 模型时,思考块与紧接的工具块在实时流式阶段无法堆叠,呈现为“两个两个分开”的块;但刷新后历史记录显示又正常。
|
|
||||||
|
|
||||||
## Debug 过程
|
|
||||||
1. 复现并对比:其他模型堆叠正常,只有 minimax 异常。
|
|
||||||
2. 查看前端堆叠逻辑:堆叠依赖 actions 顺序连续(thinking/tool)。
|
|
||||||
3. 加入前端调试日志,捕获“空白文本 action”。
|
|
||||||
4. 复现后日志显示出现 `text` action,但 `content` 为空,且正好夹在 thinking 与 tool 之间。
|
|
||||||
|
|
||||||
## 找到的问题
|
|
||||||
minimax 流式会触发 `text_start`,但没有任何 `text_chunk`(或最终内容为空),导致前端创建一个空的 `text` action,打断 `thinking -> tool` 的连续性,从而破坏堆叠。
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
前端改为:
|
|
||||||
- `text_start` 不立即创建 text action;
|
|
||||||
- 只有收到首个非空 `text_chunk`(或最终 `text_end` 有内容)时才创建;
|
|
||||||
- 无实际正文时不生成 text action,避免空 action 插入。
|
|
||||||
|
|
||||||
结果:minimax 实时堆叠恢复正常,刷新与实时显示一致。
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
# 诡异Bug记录
|
|
||||||
|
|
||||||
- 诡异程度:★★★★☆
|
|
||||||
- 发现时间:2026-02-25 13:21:48
|
|
||||||
- 场景/模块:前端聊天流式渲染(块与输出内容错位)
|
|
||||||
- 关联模型:minimax-m2.5
|
|
||||||
|
|
||||||
## 描述
|
|
||||||
使用 minimax 模型时出现“块与输出内容错位”的严重前端问题,表现为流式阶段内容与对应块不对齐。
|
|
||||||
|
|
||||||
## Debug 过程
|
|
||||||
1. 回顾近期针对 minimax 的堆叠修复(延迟创建 text action)。
|
|
||||||
2. 复现后确认错位仅发生在该修复逻辑开启时。
|
|
||||||
3. 临时回退到方案1(text_start 直接创建 text action + 堆叠时忽略空文本 action)进行验证。
|
|
||||||
|
|
||||||
## 找到的问题
|
|
||||||
延迟创建 text action 的策略会在部分 minimax 流式节奏下打乱 action 与 chunk 的对应关系,导致内容落入错误块中。
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
改回方案1:
|
|
||||||
- text_start 直接创建 text action;
|
|
||||||
- 堆叠渲染时忽略空白 text action。
|
|
||||||
|
|
||||||
结果:错位问题消失。
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
# 诡异 Bug 记录模板
|
|
||||||
|
|
||||||
每次遇到诡异问题时,在本目录新增一份 `yyyy-mm-dd_hhmmss_<简短主题>.md` 文件,内容按下列格式填写。
|
|
||||||
|
|
||||||
## 模板
|
|
||||||
|
|
||||||
```md
|
|
||||||
# 诡异Bug记录
|
|
||||||
|
|
||||||
- 诡异程度:☆☆☆☆☆(0~5星)
|
|
||||||
- 发现时间:YYYY-MM-DD HH:MM:SS
|
|
||||||
- 场景/模块:
|
|
||||||
- 关联模型:
|
|
||||||
|
|
||||||
## 描述
|
|
||||||
(简述现象与影响)
|
|
||||||
|
|
||||||
## Debug 过程
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
## 找到的问题
|
|
||||||
(最终定位的根因)
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
(最终修复手段与关键点)
|
|
||||||
```
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
778
翻译前_英文原文.txt
778
翻译前_英文原文.txt
@ -1,778 +0,0 @@
|
|||||||
【翻译前 - 英文原文】
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
1 ABSTRACT
|
|
||||||
================================================================================
|
|
||||||
While large language models (LLMs) have demonstrated impressive performance
|
|
||||||
across tasks in language understanding and interactive decision making, their
|
|
||||||
abilities for reasoning (e.g. chain-of-thought prompting) and acting (e.g. action
|
|
||||||
plan generation) have primarily been studied as separate topics. In this paper, we
|
|
||||||
explore the use of LLMs to generate both reasoning traces and task-specific actions
|
|
||||||
in an interleaved manner, allowing for greater synergy between the two: reasoning
|
|
||||||
traces help the model induce, track, and update action plans as well as handle
|
|
||||||
exceptions, while actions allow it to interface with and gather additional information
|
|
||||||
from external sources such as knowledge bases or environments. We apply our
|
|
||||||
approach, named ReAct, to a diverse set of language and decision making tasks
|
|
||||||
and demonstrate its effectiveness over state-of-the-art baselines in addition to
|
|
||||||
improved human interpretability and trustworthiness. Concretely, on question
|
|
||||||
answering (HotpotQA) and fact verification (Fever), ReAct overcomes prevalent
|
|
||||||
issues of hallucination and error propagation in chain-of-thought reasoning by
|
|
||||||
interacting with a simple Wikipedia API, and generating human-like task-solving
|
|
||||||
trajectories that are more interpretable than baselines without reasoning traces.
|
|
||||||
Furthermore, on two interactive decision making benchmarks (ALFWorld and
|
|
||||||
WebShop), ReAct outperforms imitation and reinforcement learning methods by
|
|
||||||
an absolute success rate of 34% and 10% respectively, while being prompted with
|
|
||||||
only one or two in-context examples.
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
2 INTRODUCTION
|
|
||||||
================================================================================
|
|
||||||
A unique feature of human intelligence is the ability to seamlessly combine task-oriented actions with
|
|
||||||
verbal reasoning (or inner speech, Alderson-Day & Fernyhough, 2015), which has been theorized to
|
|
||||||
play an important role in human cognition for enabling self-regulation or strategization (Vygotsky,
|
|
||||||
1987; Luria, 1965; Fernyhough, 2010) and maintaining a working memory (Baddeley, 1992). Con-
|
|
||||||
sider the example of cooking up a dish in the kitchen. Between any two specific actions, we may
|
|
||||||
reason in language in order to track progress (“now that everything is cut, I should heat up the pot of
|
|
||||||
water”), to handle exceptions or adjust the plan according to the situation (“I don’t have salt, so let
|
|
||||||
me use soy sauce and pepper instead”), and to realize when external information is needed (“how do
|
|
||||||
I prepare dough? Let me search on the Internet”). We may also act (open a cookbook to read the
|
|
||||||
recipe, open the fridge, check ingredients) to support the reasoning and to answer questions (“What
|
|
||||||
dish can I make right now?”). This tight synergy between “acting” and “reasoning” allows humans
|
|
||||||
to learn new tasks quickly and perform robust decision making or reasoning, even under previously
|
|
||||||
unseen circumstances or facing information uncertainties.
|
|
||||||
Recent results have hinted at the possibility of combining verbal reasoning with interactive decision
|
|
||||||
making in autonomous systems. On one hand, properly prompted large language models (LLMs)
|
|
||||||
have demonstrated emergent capabilities to carry out several steps of reasoning traces to derive
|
|
||||||
∗Work during Google internship. Projet page with code: https://react-lm.github.io/.
|
|
||||||
arXiv:2210.03629v3 [cs.CL] 10 Mar 2023
|
|
||||||
|
|
||||||
--- Page 2 ---
|
|
||||||
$FW7KLQN>)LUVW,QHHGWRILQGDSHSSHUVKDNHUPRUH
|
|
||||||
OLNHO\WRDSSHDULQFDELQHWVFRXQWHUWRSV
|
|
||||||
@
|
|
||||||
$FW*RWRFDELQHW
|
|
||||||
2EV2QWKHFDELQHW\RXVHHDYDVH
|
|
||||||
+HUHWKHDJHQWJRWRFDELQHWWKHQFDELQHWWKHQFRXQWHUWRSDQG
|
|
||||||
$FW*RWRFRXQWHUWRS
|
|
||||||
2EV2QWKHFRXQWHUWRS\RXVHHDDSSOHDEUHDG
|
|
||||||
|
|
||||||
DSHSSHUVKDNHUDQGDYDVH
|
|
||||||
$FW7DNHSHSSHUVKDNHUIURPFRXQWHUWRS
|
|
||||||
2EV<RXSLFNXSWKHSHSSHUVKDNHUIURPWKHFRXQWHUWRS
|
|
||||||
|
|
||||||
$FW7KLQN>1RZ,ILQGDSHSSHUVKDNHU1H[W,QHHGWR
|
|
||||||
|
|
||||||
SXWLWLQRQGUDZHU @
|
|
||||||
$FW*RWRGUDZHU
|
|
||||||
2EV'UDZHULVFORVHG
|
|
||||||
$FW2SHQGUDZHU
|
|
||||||
2EV<RXRSHQ'UDZHUª
|
|
||||||
$FW3XWSHSSHUVKDNHULQRQGUDZHU
|
|
||||||
2EV<RXSXWSHSSHUVKDNHULQRQWKHGUDZHU
|
|
||||||
D6WDQGDUG
|
|
||||||
$QVZHUL3RG
|
|
||||||
E&R75HDVRQ2QO\
|
|
||||||
7KRXJKW/HW
|
|
||||||
VWKLQNVWHS
|
|
||||||
E\VWHS $SSOH5HPRWHZDV
|
|
||||||
RULJLQDOO\GHVLJQHGWR
|
|
||||||
LQWHUDFWZLWK$SSOH79
|
|
||||||
$SSOH79FDQEHFRQWUROOHG
|
|
||||||
E\L3KRQHL3DGDQGL3RG
|
|
||||||
7RXFK6RWKHDQVZHULV
|
|
||||||
L3KRQHL3DGDQGL3RG
|
|
||||||
7RXFK
|
|
||||||
$QVZHUL3KRQHL3DGL3RG
|
|
||||||
7RXFK
|
|
||||||
F$FW2QO\
|
|
||||||
$FW6HDUFK>$SSOH5HPRWH@
|
|
||||||
2EV7KH$SSOH5HPRWHLV D
|
|
||||||
UHPRWHFRQWUROª
|
|
||||||
$FW6HDUFK>)URQW5RZ @
|
|
||||||
2EV&RXOGQRWILQG>)URQW
|
|
||||||
5RZ@6LPLODUª
|
|
||||||
$FW6HDUFK>)URQW5RZ
|
|
||||||
VRIWZDUH @
|
|
||||||
2EV)URQW5RZLVD
|
|
||||||
GLVFRQWLQXHGPHGLDFHQWHU
|
|
||||||
VRIWZDUHª
|
|
||||||
$FW)LQLVK>\HV@
|
|
||||||
G5H$FW5HDVRQ$FW
|
|
||||||
7KRXJKW,QHHGWRVHDUFK$SSOH5HPRWHDQGILQGWKH
|
|
||||||
SURJUDPLWZDVRULJLQDOO\GHVLJQHGWRLQWHUDFWZLWK
|
|
||||||
$FW6HDUFK>$SSOH5HPRWH@
|
|
||||||
2EV7KH$SSOH5HPRWHLVDUHPRWHFRQWUROLQWURGXFHGLQ
|
|
||||||
|
|
||||||
2FWREHUE\$SSOHªRULJLQDOO\GHVLJQHGWRFRQWUROWKH
|
|
||||||
|
|
||||||
)URQW5RZPHGLDFHQWHU SURJUDPª
|
|
||||||
7KRXJKW$SSOH5HPRWHZDVRULJLQDOO\GHVLJQHGWRFRQWURO
|
|
||||||
|
|
||||||
WKH)URQW5RZPHGLDFHQWHUSURJUDP,QHHGWRVHDUFK
|
|
||||||
)URQW
|
|
||||||
5RZQH[WDQGILQGZKDWRWKHUGHYLFHFDQFRQWUROLW
|
|
||||||
$FW6HDUFK>)URQW5RZ@
|
|
||||||
2EV&RXOGQRWILQG>)URQW5RZ@6LPLODU>
|
|
||||||
)URQW5RZ
|
|
||||||
|
|
||||||
6HDWWR(DUWK
|
|
||||||
|
|
||||||
)URQW5RZ0RWRUVSRUWV
|
|
||||||
|
|
||||||
)URQW5RZ
|
|
||||||
VRIWZDUH
|
|
||||||
ª@
|
|
||||||
7KRXJKW)URQW5RZLVQRWIRXQG,QHHGWRVHDUFK )URQW
|
|
||||||
5RZVRIWZDUH
|
|
||||||
$FW6HDUFK>)URQW5RZVRIWZDUH @
|
|
||||||
2EV)URQW5RZLVDGLVFRQWLQXHGPHGLDFHQWHUVRIWZDUHª
|
|
||||||
7KRXJKW)URQW5RZVRIWZDUHLVFRQWUROOHGE\DQ$SSOH
|
|
||||||
|
|
||||||
5HPRWHRUWKHNH\ERDUGIXQFWLRQNH\V 6RWKHDQVZHULV
|
|
||||||
NH\ERDUGIXQFWLRQNH\V
|
|
||||||
$FW)LQLVK>NH\ERDUGIXQFWLRQNH\V @
|
|
||||||
Ҽ
|
|
||||||
4XHVWLRQ$VLGHIURPWKH$SSOH5HPRWHZKDWRWKHUGHYLFH
|
|
||||||
|
|
||||||
FDQFRQWUROWKHSURJUDP$SSOH5HPRWHZDVRULJLQDOO\
|
|
||||||
|
|
||||||
GHVLJQHGWRLQWHUDFWZLWK"
|
|
||||||
D$FW2QO\
|
|
||||||
$FW*RWRGUDZHU
|
|
||||||
2EV7KHGUDZHULVFORVHG
|
|
||||||
$FW2SHQGUDZHU
|
|
||||||
2EV<RXRSHQWKHGUDZHU7KHGUDZHULVRSHQ,QLW
|
|
||||||
|
|
||||||
\RXVHHDGLVKVSRQJHDQGDVSRRQ
|
|
||||||
|
|
||||||
$FW*RWRVLQNEDVLQ
|
|
||||||
2EV2QWKHVLQNEDVLQ\RXVHHDGLVKVSRQJHD
|
|
||||||
|
|
||||||
VSDWXODDQGDVSRRQ
|
|
||||||
$FW7DNHSHSSHUVKDNHUIURPVLQNEDVLQ
|
|
||||||
2EV1RWKLQJKDSSHQV
|
|
||||||
$FW7DNHSHSSHUVKDNHUIURPVLQNEDVLQ
|
|
||||||
2EV1RWKLQJKDSSHQV
|
|
||||||
E5H$FW5HDVRQ$FW
|
|
||||||
Ҽ
|
|
||||||
<RXDUHLQWKHPLGGOHRIDURRP/RRNLQJTXLFNO\DURXQG
|
|
||||||
|
|
||||||
\RX\RXVHHDFDELQHWDFDELQHWDFRIIHHPDFKLQH
|
|
||||||
|
|
||||||
DFRXQWHUWRSDVWRYHEXUQHUDQGDWRDVWHU
|
|
||||||
|
|
||||||
<RXUWDVNLVWR3XWVRPHSHSSHUVKDNHURQDGUDZHU
|
|
||||||
$OI:RUOG
|
|
||||||
+RWVSRW4$
|
|
||||||
Figure 1: (1) Comparison of 4 prompting methods, (a) Standard, (b) Chain-of-thought (CoT,
|
|
||||||
Reason Only), (c) Act-only, and (d) ReAct (Reason+Act), solving a HotpotQA (Yang et al., 2018)
|
|
||||||
question. (2) Comparison of (a) Act-only and (b) ReAct prompting to solve an AlfWorld (Shridhar
|
|
||||||
et al., 2020b) game. In both domains, we omit in-context examples in the prompt, and only show task
|
|
||||||
solving trajectories generated by the model (Act, Thought) and the environment (Obs).
|
|
||||||
answers from questions in arithmetic, commonsense, and symbolic reasoning tasks (Wei et al.,
|
|
||||||
2022). However, this “chain-of-thought” reasoning is a static black box, in that the model uses
|
|
||||||
its own internal representations to generate thoughts and is not grounded in the external world,
|
|
||||||
which limits its ability to reason reactively or update its knowledge. This can lead to issues like fact
|
|
||||||
hallucination and error propagation over the reasoning process (Figure 1 (1b)). On the other hand,
|
|
||||||
recent work has explored the use of pre-trained language models for planning and acting in interactive
|
|
||||||
environments (Ahn et al., 2022; Nakano et al., 2021; Yao et al., 2020; Huang et al., 2022a), with
|
|
||||||
a focus on predicting actions via language priors. These approaches usually convert multi-modal
|
|
||||||
observations into text, use a language model to generate domain-specific actions or plans, and then
|
|
||||||
use a controller to choose or execute them. However, they do not employ language models to reason
|
|
||||||
abstractly about high-level goals or maintain a working memory to support acting, barring Huang
|
|
||||||
et al. (2022b) who perform a limited form of verbal reasoning to reiterate spatial facts about the
|
|
||||||
current state. Beyond such simple embodied tasks to interact with a few blocks, there have not been
|
|
||||||
studies on how reasoning and acting can be combined in a synergistic manner for general task solving,
|
|
||||||
and if such a combination can bring systematic benefits compared to reasoning or acting alone.
|
|
||||||
In this work, we present ReAct, a general paradigm to combine reasoning and acting with language
|
|
||||||
models for solving diverse language reasoning and decision making tasks (Figure 1). ReAct
|
|
||||||
prompts LLMs to generate both verbal reasoning traces and actions pertaining to a task in an
|
|
||||||
interleaved manner, which allows the model to perform dynamic reasoning to create, maintain, and
|
|
||||||
adjust high-level plans for acting (reason to act), while also interact with the external environments
|
|
||||||
(e.g. Wikipedia) to incorporate additional information into reasoning (act to reason).
|
|
||||||
|
|
||||||
--- Page 3 ---
|
|
||||||
We conduct empirical evaluations of ReAct and state-of-the-art baselines on four diverse benchmarks:
|
|
||||||
question answering (HotPotQA, Yang et al., 2018), fact verification (Fever, Thorne et al., 2018),
|
|
||||||
text-based game (ALFWorld, Shridhar et al., 2020b), and webpage navigation (WebShop, Yao
|
|
||||||
et al., 2022). For HotPotQA and Fever, with access to a Wikipedia API that the model can interact
|
|
||||||
with, ReAct outperforms vanilla action generation models while being competitive with chain-of-
|
|
||||||
thought reasoning (CoT) (Wei et al., 2022). The best approach overall is a combination of ReAct
|
|
||||||
and CoT that allows for the use of both internal knowledge and externally obtained information
|
|
||||||
during reasoning. On ALFWorld and WebShop, two or even one-shot ReAct prompting is able
|
|
||||||
to outperform imitation or reinforcement learning methods trained with 103 ∼105 task instances,
|
|
||||||
with an absolute improvement of 34% and 10% in success rates respectively. We also demonstrate
|
|
||||||
the importance of sparse, versatile reasoning in decision making by showing consistent advantages
|
|
||||||
over controlled baselines with actions only. Besides general applicability and performance boost,
|
|
||||||
the combination of reasoning and acting also contributes to model interpretability, trustworthiness,
|
|
||||||
and diagnosability across all domains, as humans can readily distinguish information from model’s
|
|
||||||
internal knowledge versus external environments, as well as inspect reasoning traces to understand
|
|
||||||
the decision basis of model actions.
|
|
||||||
To summarize, our key contributions are the following: (1) we introduce ReAct, a novel prompt-
|
|
||||||
based paradigm to synergize reasoning and acting in language models for general task solving; (2) we
|
|
||||||
perform extensive experiments across diverse benchmarks to showcase the advantage of ReAct in a
|
|
||||||
few-shot learning setup over prior approaches that perform either reasoning or action generation in
|
|
||||||
isolation; (3) we present systematic ablations and analysis to understand the importance of acting in
|
|
||||||
reasoning tasks, and reasoning in interactive tasks; (4) we analyze the limitations of ReAct under the
|
|
||||||
prompting setup (i.e. limited support of reasoning and acting behaviors), and perform initial finetuning
|
|
||||||
experiments showing the potential of ReAct to improve with additional training data. Scaling up
|
|
||||||
ReAct to train and operate on more tasks and combining it with complementary paradigms like
|
|
||||||
reinforcement learning could further unlock the potential of large language models.
|
|
||||||
REAC T: SYNERGIZING REASONING + ACTING
|
|
||||||
Consider a general setup of an agent interacting with an environment for task solving. At time
|
|
||||||
step t, an agent receives an observation ot ∈O from the environment and takes an action at ∈A
|
|
||||||
following some policy π(at|ct), where ct = (o1, a1, · · · , ot−1, at−1, ot) is the context to the agent.
|
|
||||||
Learning a policy is challenging when the mapping ct 7→at is highly implicit and requires extensive
|
|
||||||
computation. For example, the agent shown in Figure 1(1c) is unable to generate the correct final
|
|
||||||
action (Act 4) to finish the QA task as it requires complex reasoning over the trajectory context
|
|
||||||
(Question, Act 1-3, Obs 1-3). Similarly, the agent shown in Figure 1(2a) fails to comprehend from the
|
|
||||||
context that sinkbasin 1 does not contain peppershaker 1, thus keep producing hallucinating actions.
|
|
||||||
The idea of ReAct is simple: we augment the agent’s action space to ˆ
|
|
||||||
A = A ∪L, where L is the
|
|
||||||
space of language. An action ˆat ∈L in the language space, which we will refer to as a thought or a
|
|
||||||
reasoning trace, does not affect the external environment, thus leading to no observation feedback.
|
|
||||||
Instead, a thought ˆat aims to compose useful information by reasoning over the current context ct,
|
|
||||||
and update the context ct+1 = (ct, ˆat) to support future reasoning or acting. As shown in Figure 1,
|
|
||||||
there could be various types of useful thoughts, e.g. decomposing task goals and create action plans
|
|
||||||
(2b, Act 1; 1d, Thought 1), injecting commonsense knowledge relevant to task solving (2b, Act 1),
|
|
||||||
extracting important parts from observations (1d, Thought2, 4), track progress and transit action plans
|
|
||||||
(2b, Act 8), handle exceptions and adjust action plans (1d, Thought 3), and so on.
|
|
||||||
However, as the language space L is unlimited, learning in this augmented action space is difficult
|
|
||||||
and requires strong language priors. In this paper, we mainly focus on the setup where a frozen
|
|
||||||
large language model, PaLM-540B (Chowdhery et al., 2022)1, is prompted with few-shot in-context
|
|
||||||
examples to generate both domain-specific actions and free-form language thoughts for task solving
|
|
||||||
(Figure 1 (1d), (2b)). Each in-context example is a human trajectory of actions, thoughts, and
|
|
||||||
environment observations to solve a task instance (see Appendix C). For the tasks where reasoning is
|
|
||||||
of primary importance (Figure 1(1)), we alternate the generation of thoughts and actions so that the
|
|
||||||
task-solving trajectory consists of multiple thought-action-observation steps. In contrast, for decision
|
|
||||||
making tasks that potentially involve a large number of actions (Figure 1(2)), thoughts only need to
|
|
||||||
1We show some GPT-3 (Brown et al., 2020) results in Appendix A.1, which outperforms PaLM-540B.
|
|
||||||
|
|
||||||
--- Page 4 ---
|
|
||||||
appear sparsely in the most relevant positions of a trajectory, so we let the language model decide the
|
|
||||||
asynchronous occurrence of thoughts and actions for itself.
|
|
||||||
Since decision making and reasoning capabilities are integrated into a large language model, ReAct
|
|
||||||
enjoys several unique features: A) Intuitive and easy to design: Designing ReAct prompts is
|
|
||||||
straightforward as human annotators just type down their thoughts in language on top of their actions
|
|
||||||
taken. No ad-hoc format choice, thought design, or example selection is used in this paper. We detail
|
|
||||||
prompt design for each task in Sections 3 and 4. B) General and flexible: Due to the flexible thought
|
|
||||||
space and thought-action occurrence format, ReAct works for diverse tasks with distinct action
|
|
||||||
spaces and reasoning needs, including but not limited to QA, fact verification, text game, and web
|
|
||||||
navigation. C) Performant and robust: ReAct shows strong generalization to new task instances
|
|
||||||
while learning solely from one to six in-context examples, consistently outperforming baselines with
|
|
||||||
only reasoning or acting across different domains. We also show in Section 3 additional benefits
|
|
||||||
when finetuning is enabled, and in Section 4 how ReAct performance is robust to prompt selections.
|
|
||||||
D) Human aligned and controllable: ReAct promises an interpretable sequential decision making
|
|
||||||
and reasoning process where humans can easily inspect reasoning and factual correctness. Moreover,
|
|
||||||
humans can also control or correct the agent behavior on the go by thought editing, as shown in
|
|
||||||
Figure 5 in Section 4.
|
|
||||||
KNOWLEDGE-INTENSIVE REASONING TASKS
|
|
||||||
We begin with knowledge-intensive reasoning tasks like multi-hop question answering and fact
|
|
||||||
verification. As shown in Figure 1(1d), by interacting with a Wikipedia API, ReAct is able to
|
|
||||||
retrieve information to support reasoning, while also use reasoning to target what to retrieve next,
|
|
||||||
demonstrating a synergy of reasoning and acting.
|
|
||||||
3.1
|
|
||||||
SETUP
|
|
||||||
Domains
|
|
||||||
We consider two datasets challenging knowledge retrieval and reasoning: (1) Hot-
|
|
||||||
PotQA (Yang et al., 2018), a multi-hop question answering benchmark that requires reasoning
|
|
||||||
over two or more Wikipedia passages, and (2) FEVER (Thorne et al., 2018), a fact verification
|
|
||||||
benchmark where each claim is annotated SUPPORTS, REFUTES, or NOT ENOUGH INFO, based
|
|
||||||
on if there exists a Wikipedia passage to verify the claim. In this work, we operate in a question-only
|
|
||||||
setup for both tasks, where models only receive the question/claim as input without access to support
|
|
||||||
paragraphs, and have to rely on their internal knowledge or retrieve knowledge via interacting with
|
|
||||||
an external environment to support reasoning.
|
|
||||||
Action Space
|
|
||||||
We design a simple Wikipedia web API with three types of actions to support
|
|
||||||
interactive information retrieval: (1) search[entity], which returns the first 5 sentences from
|
|
||||||
the corresponding entity wiki page if it exists, or else suggests top-5 similar entities from the
|
|
||||||
Wikipedia search engine, (2) lookup[string], which would return the next sentence in the page
|
|
||||||
containing string, simulating Ctrl+F functionality on the browser. (3) finish[answer], which
|
|
||||||
would finish the current task with answer. We note that this action space mostly can only retrieve a
|
|
||||||
small part of a passage based on exact passage name, which is significantly weaker than state-of-the-
|
|
||||||
art lexical or neural retrievers. The purpose is to simulate how humans would interact with Wikipedia,
|
|
||||||
and force models to retrieve via explicit reasoning in language.
|
|
||||||
3.2
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
3 METHOD
|
|
||||||
================================================================================
|
|
||||||
S
|
|
||||||
ReAct Prompting
|
|
||||||
For HotpotQA and Fever, we randomly select 6 and 3 cases2 from the training
|
|
||||||
set and manually compose ReAct-format trajectories to use as few-shot exemplars in the prompts.
|
|
||||||
Similar to Figure 1(d), each trajectory consists of multiple thought-action-observation steps (i.e. dense
|
|
||||||
thought), where free-form thoughts are used for various purposes. Specifically, we use a combination
|
|
||||||
of thoughts that decompose questions (“I need to search x, find y, then find z”), extract information
|
|
||||||
from Wikipedia observations (“x was started in 1844”, “The paragraph does not tell x”), perform
|
|
||||||
commonsense (“x is not y, so z must instead be...”) or arithmetic reasoning (“1844 < 1989”), guide
|
|
||||||
2We find more examples do not improve performance.
|
|
||||||
|
|
||||||
--- Page 5 ---
|
|
||||||
Prompt Methoda
|
|
||||||
HotpotQA
|
|
||||||
Fever
|
|
||||||
(EM)
|
|
||||||
(Acc)
|
|
||||||
Standard
|
|
||||||
28.7
|
|
||||||
57.1
|
|
||||||
CoT (Wei et al., 2022)
|
|
||||||
29.4
|
|
||||||
56.3
|
|
||||||
CoT-SC (Wang et al., 2022a)
|
|
||||||
33.4
|
|
||||||
60.4
|
|
||||||
Act
|
|
||||||
25.7
|
|
||||||
58.9
|
|
||||||
ReAct
|
|
||||||
27.4
|
|
||||||
60.9
|
|
||||||
CoT-SC →ReAct
|
|
||||||
34.2
|
|
||||||
64.6
|
|
||||||
ReAct→CoT-SC
|
|
||||||
35.1
|
|
||||||
62.0
|
|
||||||
Supervised SoTAb
|
|
||||||
67.5
|
|
||||||
89.5
|
|
||||||
Table 1: PaLM-540B prompting results on
|
|
||||||
HotpotQA and Fever.
|
|
||||||
aHotpotQA EM is 27.1, 28.9, 33.8 for Standard, CoT,
|
|
||||||
CoT-SC in Wang et al. (2022b).
|
|
||||||
b(Zhu et al., 2021; Lewis et al., 2020)
|
|
||||||
0
|
|
||||||
20
|
|
||||||
#CoT-SC trials
|
|
||||||
26
|
|
||||||
28
|
|
||||||
30
|
|
||||||
32
|
|
||||||
34
|
|
||||||
HotpotQA EM
|
|
||||||
0
|
|
||||||
20
|
|
||||||
#CoT-SC trials
|
|
||||||
47.5
|
|
||||||
50.0
|
|
||||||
52.5
|
|
||||||
55.0
|
|
||||||
57.5
|
|
||||||
60.0
|
|
||||||
62.5
|
|
||||||
65.0
|
|
||||||
Fever Acc
|
|
||||||
Method
|
|
||||||
CoT-SC -> ReAct
|
|
||||||
ReAct -> CoT-SC
|
|
||||||
CoT-SC
|
|
||||||
ReAct
|
|
||||||
CoT
|
|
||||||
Figure 2: PaLM-540B prompting results with respect to
|
|
||||||
number of CoT-SC samples used.
|
|
||||||
search reformulation (“maybe I can search/look up x instead”), and synthesize the final answer (“...so
|
|
||||||
the answer is x”). See Appendix C for more details.
|
|
||||||
Baselines
|
|
||||||
We systematically ablate ReAct trajectories to build prompts for multiple baselines (with
|
|
||||||
formats as Figure 1(1a-1c)): (a) Standard prompting (Standard), which removes all thoughts,
|
|
||||||
actions, observations in ReAct trajectories. (b) Chain-of-thought prompting (CoT) (Wei et al.,
|
|
||||||
2022), which removes actions and observations and serve as a reasoning-only baseline. We also
|
|
||||||
build a self-consistency baseline (CoT-SC) (Wang et al., 2022a;b) by sampling 21 CoT trajectories
|
|
||||||
with decoding temperature 0.7 during inference and adopting the majority answer, which is found to
|
|
||||||
consistently boost performance over CoT. (c) Acting-only prompt (Act), which removes thoughts
|
|
||||||
in ReAct trajectories, loosely resembling how WebGPT (Nakano et al., 2021) interacts with the
|
|
||||||
Internet to answer questions, though it operates on a different task and action space, and uses imitation
|
|
||||||
and reinforcement learning instead of prompting.
|
|
||||||
Combining Internal and External Knowledge
|
|
||||||
As will be detail in Section 3.3, we observe that
|
|
||||||
the problem solving process demonstrated by ReAct is more factual and grounded, whereas CoT
|
|
||||||
is more accurate in formulating reasoning structure but can easily suffer from hallucinated facts
|
|
||||||
or thoughts. We therefore propose to incorporate ReAct and CoT-SC, and let the model decide
|
|
||||||
when to switch to the other method based on the following heuristics: A) ReAct →CoT-SC: when
|
|
||||||
ReAct fails to return an answer within given steps, back off to CoT-SC. We set 7 and 5 steps for
|
|
||||||
HotpotQA and FEVER respectively as we find more steps will not improve ReAct performance3.
|
|
||||||
B) CoT-SC →ReAct: when the majority answer among n CoT-SC samples occurs less than n/2
|
|
||||||
times (i.e. internal knowledge might not support the task confidently), back off to ReAct.
|
|
||||||
Finetuning
|
|
||||||
Due to the challenge of manually annotating reasoning traces and actions at scale,
|
|
||||||
we consider a bootstraping approach similar to Zelikman et al. (2022), using 3,000 trajectories
|
|
||||||
with correct answers generated by ReAct (also for other baselines) to finetune smaller language
|
|
||||||
models (PaLM-8/62B) to decode trajectories (all thoughts, actions, observations) conditioned on
|
|
||||||
input questions/claims. More details are in Appendix B.1.
|
|
||||||
3.3
|
|
||||||
RESULTS AND OBSERVATIONS
|
|
||||||
ReAct outperforms Act consistently
|
|
||||||
Table 1 shows HotpotQA and Fever results using PaLM-
|
|
||||||
540B as the base model with different prompting methods. We note that ReAct is better than Act
|
|
||||||
on both tasks, demonstrating the value of reasoning to guide acting, especially for synthesizing the
|
|
||||||
final answer, as shown in Figure 1 (1c-d). Fine-tuning results 3 also confirm the benefit of reasoning
|
|
||||||
traces for more informed acting.
|
|
||||||
3Of all trajectories with correct final answers, those with 7 steps on HotpotQA and 5 steps on FEVER only
|
|
||||||
take up 0.84% and 1.33% respectively.
|
|
||||||
|
|
||||||
--- Page 6 ---
|
|
||||||
Type
|
|
||||||
Definition
|
|
||||||
ReAct
|
|
||||||
CoT
|
|
||||||
Success
|
|
||||||
True positive
|
|
||||||
Correct reasoning trace and facts
|
|
||||||
94%
|
|
||||||
86%
|
|
||||||
False positive
|
|
||||||
Hallucinated reasoning trace or facts
|
|
||||||
6%
|
|
||||||
14%
|
|
||||||
Failure
|
|
||||||
Reasoning error
|
|
||||||
Wrong reasoning trace (including failing to recover from repetitive steps)
|
|
||||||
47%
|
|
||||||
16%
|
|
||||||
Search result error
|
|
||||||
Search return empty or does not contain useful information
|
|
||||||
23%
|
|
||||||
-
|
|
||||||
Hallucination
|
|
||||||
Hallucinated reasoning trace or facts
|
|
||||||
0%
|
|
||||||
56%
|
|
||||||
Label ambiguity
|
|
||||||
Right prediction but did not match the label precisely
|
|
||||||
29%
|
|
||||||
28%
|
|
||||||
Table 2: Types of success and failure modes of ReAct and CoT on HotpotQA, as well as their
|
|
||||||
percentages in randomly selected examples studied by human.
|
|
||||||
ReAct vs. CoT
|
|
||||||
On the other hand, ReAct outperforms CoT on Fever (60.9 vs. 56.3) and slightly
|
|
||||||
lags behind CoT on HotpotQA (27.4 vs. 29.4). Fever claims for SUPPORTS/REFUTES might only
|
|
||||||
differ by a slight amount (see Appendix D.1), so acting to retrieve accurate and up-to-date knowledge
|
|
||||||
is vital. To better understand the behavioral difference between ReAct and CoT on HotpotQA, we
|
|
||||||
randomly sampled 50 trajectories with correct and incorrect answers (judged by EM) from ReAct
|
|
||||||
and CoT respectively (thus 200 examples in total), and manually labeled their success and failure
|
|
||||||
modes in Table 2. Some key observations are as follows:
|
|
||||||
A) Hallucination is a serious problem for CoT, resulting in much higher false positive rate than
|
|
||||||
ReAct (14% vs. 6%) in success mode, and make up its major failure mode (56%). In contrast, the
|
|
||||||
problem solving trajectory of ReActis more grounded, fact-driven, and trustworthy, thanks to the
|
|
||||||
access of an external knowledge base.
|
|
||||||
B) While interleaving reasoning, action and observation steps improves ReAct’s grounded-
|
|
||||||
ness and trustworthiness, such a structural constraint also reduces its flexibility in formulating
|
|
||||||
reasoning steps, leading to more reasoning error rate than CoT. we note that there is one frequent
|
|
||||||
error pattern specific to ReAct, in which the model repetitively generates the previous thoughts and
|
|
||||||
actions, and we categorize it as part of “reasoning error” as the model fails to reason about what the
|
|
||||||
proper next action to take and jump out of the loop4.
|
|
||||||
C) For ReAct, successfully retrieving informative knowledge via search is critical. Non-
|
|
||||||
informative search, which counts for 23% of the error cases, derails the model reasoning and gives
|
|
||||||
it a hard time to recover and reformulate thoughts. This is perhaps an expected trade-off between
|
|
||||||
factuality and flexibility, which motivates our proposed strategies of combining two methods.
|
|
||||||
We provide examples for each success and failure modes in Appendix E.1. We also find some
|
|
||||||
HotpotQA questions may contain outdated answer labels, see Figure 4 for example.
|
|
||||||
ReAct + CoT-SC perform best for prompting LLMs
|
|
||||||
Also shown in Table 1, the best prompting
|
|
||||||
method on HotpotQA and Fever are ReAct →CoT-SC and CoT-SC →ReAct respectively.
|
|
||||||
Furthermore, Figure 2 shows how different methods perform with respect to the number of CoT-SC
|
|
||||||
samples used. While two ReAct + CoT-SC methods are advantageous at one task each, they both
|
|
||||||
significantly and consistently outperform CoT-SC across different number of samples, reaching
|
|
||||||
CoT-SC performance with 21 samples using merely 3-5 samples. These results indicate the value of
|
|
||||||
properly combining model internal knowledge and external knowledge for reasoning tasks.
|
|
||||||
ReAct performs best for fine-tuning
|
|
||||||
Figure 3 shows the scaling effect of prompting/finetuning
|
|
||||||
four methods (Standard, CoT, Act, ReAct) on HotpotQA. With PaLM-8/62B, prompting ReAct
|
|
||||||
performs worst among four methods due to the difficulty to learn both reasoning and acting from
|
|
||||||
in-context examples. However, when finetuned with just 3,000 examples, ReAct becomes the best
|
|
||||||
method among the four, with PaLM-8B finetuned ReAct outperforming all PaLM-62B prompting
|
|
||||||
methods, and PaLM-62B finetuned ReAct outperforming all 540B prompting methods. In contrast,
|
|
||||||
finetuning Standard or CoT is significantly worse than finetuning ReAct or Act for both PaLM-
|
|
||||||
8/62B, as the former essentially teaches models to memorize (potentially halluincated) knowledge
|
|
||||||
facts, and the latter teaches models how to (reason and) act to access information from Wikipedia, a
|
|
||||||
more generalizable skill for knowledge reasoning. As all prompting methods are still significantly
|
|
||||||
far from domain-specific state-of-the-art approaches (Table 1), we believe finetuning with more
|
|
||||||
human-written data might be a better way to unleash the power of ReAct.
|
|
||||||
4We suspect that this could be due to the sub-optimal greedy decoding procedure, and future work using
|
|
||||||
better decoding (e.g. beam search) might help address this issue.
|
|
||||||
|
|
||||||
--- Page 7 ---
|
|
||||||
8b
|
|
||||||
62b
|
|
||||||
540b
|
|
||||||
size
|
|
||||||
0
|
|
||||||
20
|
|
||||||
25
|
|
||||||
30
|
|
||||||
HotpotQA EM
|
|
||||||
learning = prompt
|
|
||||||
8b
|
|
||||||
62b
|
|
||||||
540b
|
|
||||||
size
|
|
||||||
learning = finetune
|
|
||||||
Method
|
|
||||||
Standard
|
|
||||||
CoT
|
|
||||||
Act
|
|
||||||
ReAct
|
|
||||||
Figure 3: Scaling results for prompting and finetuning on HotPotQA with ReAct (ours) and baselines.
|
|
||||||
DECISION MAKING TASKS
|
|
||||||
We also test ReAct on two language-based interactive decision-making tasks, ALFWorld and
|
|
||||||
WebShop, both of which feature complex environments that require agents to act over long horizons
|
|
||||||
with sparse rewards, warranting the need for reasoning to act and explore effectively.
|
|
||||||
ALFWorld
|
|
||||||
ALFWorld (Shridhar et al., 2020b) (Figure 1(2)) is a synthetic text-based game designed
|
|
||||||
to align with the embodied ALFRED benchmark (Shridhar et al., 2020a). It includes 6 types of
|
|
||||||
tasks in which an agent needs to achieve a high-level goal (e.g. examine paper under desklamp) by
|
|
||||||
navigating and interacting with a simulated household via text actions (e.g. go to coffeetable 1, take
|
|
||||||
paper 2, use desklamp 1). A task instance can have more than 50 locations and take an expert policy
|
|
||||||
more than 50 steps to solve, thus challenging an agent to plan and track subgoals, as well as explore
|
|
||||||
systematically (e.g. check all desks one by one for desklamp). In particular, one challenge built into
|
|
||||||
ALFWorld is the need to determine likely locations for common household items (e.g. desklamps will
|
|
||||||
likely be on desks, shelfs, or dressers), making this environment a good fit for LLMs to exploit their
|
|
||||||
pretrained commonsense knowledge. To prompt ReAct, we randomly annotate three trajectories
|
|
||||||
from the training set for each task type, where each trajectory includes sparse thoughts that (1)
|
|
||||||
decompose the goal, (2) track subgoal completion, (3) determine the next subgoal, and (4) reason via
|
|
||||||
commonsense where to find an object and what to do with it. We show prompts used for ALFWorld
|
|
||||||
in Appendix C.4. Following Shridhar et al. (2020b), we evaluate on 134 unseen evaluation games
|
|
||||||
in a task-specific setup. For robustness, we construct 6 prompts for each task type through each
|
|
||||||
permutation of 2 annotated trajectories from the 3 we annotate. Act prompts are constructed using
|
|
||||||
the same trajectories, but without thoughts — since task instances are randomly chosen from the
|
|
||||||
training set, it favors neither ReAct nor Act and provides a fair and controlled comparison to test the
|
|
||||||
importance of sparse thoughts. For baselines, we use BUTLER (Shridhar et al., 2020b), an imitation
|
|
||||||
learning agent trained on 105 expert trajectories for each task type5.
|
|
||||||
WebShop
|
|
||||||
Can ReAct also interact with noisy real-world language environments for practical
|
|
||||||
applications? We investigate WebShop (Yao et al., 2022), a recently proposed online shopping
|
|
||||||
website environment with 1.18M real-world products and 12k human instructions. Unlike ALFWorld,
|
|
||||||
Webshop contains a high variety of structured and unstructured texts (e.g. product titles, descriptions,
|
|
||||||
and options crawled from Amazon), and requires an agent to purchase a product based on a user
|
|
||||||
instruction (e.g. “I am looking for a nightstand with drawers. It should have a nickel finish, and
|
|
||||||
priced lower than $140”) through web interactions (e.g. search “nightstand drawers”, choose buttons
|
|
||||||
such as “color: modern-nickel-white” or “back to search”). This task is evaluated by average score
|
|
||||||
(percentage of desired attributes covered by the chosen product averaged across all episodes) and
|
|
||||||
success rate (percentage of episodes where the chosen product satisfies all requirements) on 500 test
|
|
||||||
instructions. We formulate Act prompts with actions to search, choose product, choose options,
|
|
||||||
and buy, with ReAct prompts additionally reasoning to determine what to explore, when to buy,
|
|
||||||
and what products options are relevant to the instruction. See Table 6 for an example prompt, and
|
|
||||||
Table 10 for model predictions in the Appendix. We compare to an imitation learning (IL) method
|
|
||||||
5Micheli & Fleuret (2021) finetuned a GPT-2 model on 3553 task instances and achieved a much improved
|
|
||||||
performance than BUTLER, but it is trained on all task types, thus not included as a baseline.
|
|
||||||
|
|
||||||
--- Page 8 ---
|
|
||||||
Method
|
|
||||||
Pick
|
|
||||||
Clean
|
|
||||||
Heat
|
|
||||||
Cool
|
|
||||||
Look
|
|
||||||
Pick 2
|
|
||||||
All
|
|
||||||
Act (best of 6)
|
|
||||||
88
|
|
||||||
42
|
|
||||||
74
|
|
||||||
67
|
|
||||||
72
|
|
||||||
41
|
|
||||||
45
|
|
||||||
ReAct (avg)
|
|
||||||
65
|
|
||||||
39
|
|
||||||
83
|
|
||||||
76
|
|
||||||
55
|
|
||||||
24
|
|
||||||
57
|
|
||||||
ReAct (best of 6)
|
|
||||||
92
|
|
||||||
58
|
|
||||||
96
|
|
||||||
86
|
|
||||||
78
|
|
||||||
41
|
|
||||||
71
|
|
||||||
ReAct-IM (avg)
|
|
||||||
55
|
|
||||||
59
|
|
||||||
60
|
|
||||||
55
|
|
||||||
23
|
|
||||||
24
|
|
||||||
48
|
|
||||||
ReAct-IM (best of 6)
|
|
||||||
62
|
|
||||||
68
|
|
||||||
87
|
|
||||||
57
|
|
||||||
39
|
|
||||||
33
|
|
||||||
53
|
|
||||||
BUTLERg (best of 8)
|
|
||||||
33
|
|
||||||
26
|
|
||||||
70
|
|
||||||
76
|
|
||||||
17
|
|
||||||
22
|
|
||||||
BUTLER (best of 8)
|
|
||||||
46
|
|
||||||
39
|
|
||||||
74
|
|
||||||
100
|
|
||||||
22
|
|
||||||
24
|
|
||||||
37
|
|
||||||
Table 3: AlfWorld task-specific success rates (%). BUTLER and
|
|
||||||
BUTLERg results are from Table 4 of Shridhar et al. (2020b). All
|
|
||||||
methods use greedy decoding, except that BUTLER uses beam search.
|
|
||||||
Method
|
|
||||||
Score
|
|
||||||
SR
|
|
||||||
Act
|
|
||||||
62.3
|
|
||||||
30.1
|
|
||||||
ReAct
|
|
||||||
66.6
|
|
||||||
40.0
|
|
||||||
IL
|
|
||||||
59.9
|
|
||||||
29.1
|
|
||||||
IL+RL
|
|
||||||
62.4
|
|
||||||
28.7
|
|
||||||
Human
|
|
||||||
82.1
|
|
||||||
59.6
|
|
||||||
Expert
|
|
||||||
Table 4: Score and suc-
|
|
||||||
cess rate (SR) on Web-
|
|
||||||
shop. IL/IL+RL taken
|
|
||||||
from Yao et al. (2022).
|
|
||||||
trained with 1,012 human annotated trajectories, and a imitation + reinforcement learning (IL + RL)
|
|
||||||
method additionally trained with 10,587 training instructions.
|
|
||||||
Results
|
|
||||||
ReAct outperforms Act on both ALFWorld (Table 3) and Webshop (Table 4). On
|
|
||||||
ALFWorld, the best ReAct trial achieves an average success rate of 71%, significantly outperforming
|
|
||||||
the best Act (45%) and BUTLER (37%) trials. In fact, even the worse ReAct trial (48%) beats
|
|
||||||
the best trial of both methods. Moreover, the advantage of ReAct over Act is consistent across
|
|
||||||
six controlled trials, with relative performance gain ranging from 33% to 90% and averaging 62%.
|
|
||||||
Qualitatively, we saw that, without any thoughts at all, Act fails to correctly decompose goals
|
|
||||||
into smaller subgoals, or loses track of the current state of the environment. Example trajectories
|
|
||||||
comparing ReAct and Act can be found in Appendix D.2.1 and Appendix D.2.2.
|
|
||||||
On Webshop, one-shot Act prompting already performs on par with IL and IL+RL methods. With
|
|
||||||
additional sparse reasoning, ReAct achieves significantly better performance, with an absolute 10%
|
|
||||||
improvement over the previous best success rate. By checking examples, we find that ReAct is more
|
|
||||||
likely to identify instruction-relevant products and options by reasoning to bridge the gap between
|
|
||||||
noisy observations and actions (e.g. “For ‘space-saving ottoman bench for living room’, the item
|
|
||||||
has options ‘39x18x18inch’ and ‘blue’ and seems good to buy.”). However, existing methods are
|
|
||||||
still far from the performance of expert humans (Table 4), who perform significantly more product
|
|
||||||
explorations and query re-formulations that are still challenging for prompting-based methods.
|
|
||||||
On the value of internal reasoning vs. external feedback
|
|
||||||
To our knowledge, ReAct is the first
|
|
||||||
demonstration of combined reasoning and action using an LLM applied to an interactive environment
|
|
||||||
within a closed-loop system. Perhaps the closest prior work is Inner Monologue (IM), from Huang
|
|
||||||
et al. (2022b), in which actions from an embodied agent are motivated by an eponymous “inner
|
|
||||||
monologue”. However, IM’s “inner monologue” is limited to observations of the environment
|
|
||||||
state and what needs to be completed by the agent for the goal to be satisfied. In contrast, the
|
|
||||||
reasoning traces in ReAct for decision making is flexible and sparse, allowing diverse reasoning
|
|
||||||
types (see Section 2) to be induced for different tasks.
|
|
||||||
To demonstrate the differences between ReAct and IM, and to highlight the importance of internal
|
|
||||||
reasoning vs. simple reactions to external feedback, we ran an ablation experiment using a thought
|
|
||||||
pattern composed of IM-like dense external feedback. As can be seen in Table 3, ReAct substantially
|
|
||||||
outperforms IM-style prompting (ReAct-IM) (71 vs. 53 overall success rate), with consistent
|
|
||||||
advantages on five out of six tasks. Qualitatively, we observed that ReAct-IM often made mistakes
|
|
||||||
in identifying when subgoals were finished, or what the next subgoal should be, due to a lack of high-
|
|
||||||
level goal decomposition. Additionally, many ReAct-IM trajectories struggled to determine where
|
|
||||||
an item would likely be within the ALFWorld environment, due to a lack of commonsense reasoning.
|
|
||||||
Both shortcomings can be addressed in the ReAct paradigm. More details about ReAct-IM is in
|
|
||||||
Appendix B.2. An example prompt for ReAct-IM can be found in Appendix C.4, and an example
|
|
||||||
trajectory in Appendix D.2.3.
|
|
||||||
|
|
||||||
--- Page 9 ---
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
4 RELATED WORK
|
|
||||||
================================================================================
|
|
||||||
Language model for reasoning
|
|
||||||
Perhaps the most well-known work of using LLMs for reasoning
|
|
||||||
is Chain-of-Thought (CoT) (Wei et al., 2022), which reveals the ability of LLMs to formulate their
|
|
||||||
own “thinking procedure” for problem solving. Several follow-up works have since been performed,
|
|
||||||
including least-to-most prompting for solving complicated tasks (Zhou et al., 2022), zero-shot-
|
|
||||||
CoT (Kojima et al., 2022), and reasoning with self-consistency (Wang et al., 2022a). Recently,
|
|
||||||
(Madaan & Yazdanbakhsh, 2022) systematically studied the formulation and structure of CoT, and
|
|
||||||
observed that the presence of symbols, patterns and texts is crucial to the effectiveness of CoT. Other
|
|
||||||
work has also been extended to more sophisticated reasoning architecture beyond simple prompting.
|
|
||||||
For example Selection-Inference (Creswell et al., 2022) divides the reasoning process into two steps
|
|
||||||
of “selection” and “inference”. STaR (Zelikman et al., 2022) bootstraps the reasoning process by
|
|
||||||
finetuning the model on correct rationales generated by the model itself. Faithful reasoning (Creswell
|
|
||||||
& Shanahan, 2022) decomposes multi-step reasoning into three steps, each performed by a dedicated
|
|
||||||
LM respectively. Similar approaches like Scratchpad (Nye et al., 2021), which finetunes a LM on
|
|
||||||
intermediate computation steps, also demonstrate improvement on multi-step computation problems.
|
|
||||||
In contrast to these methods, ReAct performs more than just isolated, fixed reasoning, and integrates
|
|
||||||
model actions and their corresponding observations into a coherent stream of inputs for the model to
|
|
||||||
reason more accurately and tackle tasks beyond reasoning (e.g. interactive decision making).
|
|
||||||
Language model for decision making
|
|
||||||
The strong capability of LLMs has enabled them to perform
|
|
||||||
tasks beyond language generation, and it is becoming more popular to take advantage of LLMs as a
|
|
||||||
policy model for decision making, especially in interactive environments. WebGPT (Nakano et al.,
|
|
||||||
2021) uses an LM to interact with web browsers, navigate through web pages, and infer answers to
|
|
||||||
complicated questions from ELI5 (Fan et al., 2019). In comparison to ReAct, WebGPT does not
|
|
||||||
explicitly model the thinking and reasoning procedure, instead rely on expensive human feedback for
|
|
||||||
reinforcement learning. In conversation modeling, chatbots like BlenderBot (Shuster et al., 2022b)
|
|
||||||
and Sparrow (Glaese et al., 2022) and task-oriented dialogue systems like SimpleTOD (Hosseini-Asl
|
|
||||||
et al., 2020) also train LMs to make decision about API calls. Unlike ReAct, they do not explicitly
|
|
||||||
consider the reasoning procedure either, and also relies on expensive datasets and human feedback
|
|
||||||
collections for policy learning. In contrast, ReAct learns a policy in a much cheaper way, since the
|
|
||||||
decision making process only requires language description of the reasoning procedure.6
|
|
||||||
LLMS have also been increasingly employed in interactive and embodied environments for planning
|
|
||||||
and decision making. Perhaps most relevant to ReAct in this respect are SayCan (Ahn et al., 2022)
|
|
||||||
and Inner Monologue (Huang et al., 2022b), which use LLMs for robotic action planning and decision
|
|
||||||
making. In SayCan, LLMs were prompted to directly predict possible actions a robot can take, which
|
|
||||||
is then reranked by an affordance model grounded on the visual environments for final prediction.
|
|
||||||
Inner Monologue made further improvements by adding the eponymous “inner monologue", which is
|
|
||||||
implemented as injected feedback from the environment. To our knowledge, Inner Monologue is the
|
|
||||||
first work that demonstrates such a closed-loop system, which ReAct builds on. However, we argue
|
|
||||||
that Inner Monologue does not truly comprise of inner thoughts — this is elaborated in Section 4. We
|
|
||||||
also note that leveraging language as semantically-rich inputs in the process of interactive decision
|
|
||||||
making has been shown to be successful under other settings (Abramson et al., 2020; Karamcheti
|
|
||||||
et al., 2021; Huang et al., 2022a; Li et al., 2022). It is becoming more evident that with the help of
|
|
||||||
LLMs, language as a fundamental cognitive mechanism will play a critical role in interaction and
|
|
||||||
decision making. What is more, progress in LLMs has also inspired the development of versatile and
|
|
||||||
generalist agents like Reed et al. (2022).
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
5 CONCLUSIONS
|
|
||||||
================================================================================
|
|
||||||
We have proposed ReAct – a simple yet effective method for synergizing reasoning and acting in
|
|
||||||
large language models. Through a diverse set of experiments on multi-hop question-answering, fact
|
|
||||||
checking, and interactive decision-making tasks, we show that ReAct leads to superior performance
|
|
||||||
with interpretable decision traces. Despite the simplicity of our method, complex tasks with large
|
|
||||||
action spaces require more demonstrations to learn well, which unfortunately can easily go beyond
|
|
||||||
the input length limit of in-context learning. We explore the fine-tuning approach on HotpotQA
|
|
||||||
6Human feedback can also be incorporated in a complementary manner but we leave it for future work.
|
|
||||||
|
|
||||||
--- Page 10 ---
|
|
||||||
with initial promising results, but learning from more high-quality human annotations will be the
|
|
||||||
desiderata to further improve the performance. Scaling up ReAct with multi-task training and
|
|
||||||
combining it with complementary paradigms like reinforcement learning could result in stronger
|
|
||||||
agents that further unlock the potential of LLMs for more applications.
|
|
||||||
ACKNOWLEDGMENTS
|
|
||||||
We thank the support and feedback of many people from Google Brain team and Princeton NLP
|
|
||||||
Group. This work was supported in part by the National Science Foundation under Grant No.
|
|
||||||
2107048. Any opinions, findings, and conclusions or recommendations expressed in this material are
|
|
||||||
those of the author(s) and do not necessarily reflect the views of the National Science Foundation.
|
|
||||||
REPRODUCIBILITY STATEMENT
|
|
||||||
Our main experiments are done on PaLM (Chowdhery et al., 2022), which is not an openly accessible
|
|
||||||
model yet. To increase reproducibility, we have included all used prompts in Appendix C, additional
|
|
||||||
experiments using GPT-3 (Brown et al., 2020) in Appendix A.1, and associated GPT-3 ReAct
|
|
||||||
prompting code at https://anonymous.4open.science/r/ReAct-2268/.
|
|
||||||
ETHICS STATEMENT
|
|
||||||
ReAct prompts large language models to generate more human interpretable, diagnosable, and
|
|
||||||
controllable task-solving trajectories than previous methods. However, hooking up a large language
|
|
||||||
model with an action space to interact with external environments (e.g. the web, physical environ-
|
|
||||||
ments) has potential dangers, e.g. looking up inappropriate or private information, or taking harmful
|
|
||||||
actions in an environment. Our experiments minimize such risks by limiting the interactions to
|
|
||||||
specific websites (Wikipedia or WebShop) that are free of private information, without any dangerous
|
|
||||||
actions in the action space design (i.e. models cannot really buy products on WebShop the research
|
|
||||||
benchmark, or edit Wikipedia). We believe researchers should be aware of such risks before designing
|
|
||||||
more extensive experiments in the future.
|
|
||||||
50
翻译后_中文译文.txt
50
翻译后_中文译文.txt
@ -1,50 +0,0 @@
|
|||||||
【翻译后 - 中文译文】
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
1 摘要
|
|
||||||
================================================================================
|
|
||||||
大型语言模型(LLMs)在语言理解和交互式决策等任务上展现出卓越的性能,但其推理能力(如思维链提示)和行动能力(如行动计划生成)主要作为独立主题进行研究。本文探索利用大型语言模型以交错方式生成推理轨迹和任务特定行动,使两者之间产生更大协同效应:推理轨迹帮助模型诱导、跟踪和更新行动计划以及处理异常情况,而行动则使其能够与外部来源(如知识库或环境)进行交互并收集额外信息。我们将这种名为ReAct的方法应用于多样化的语言和决策任务,证明其相对于最先进基线方法的有效性,同时提高了人类可解释性和可信度。具体而言,在问答任务(HotpotQA)和事实验证任务(Fever)中,ReAct通过与简单的维基百科API交互,克服了思维链推理中普遍存在的幻觉和错误传播问题,并生成比没有推理轨迹的基线方法更具可解释性的类人任务解决轨迹。此外,在两个交互式决策基准测试(ALFWorld和WebShop)中,ReAct分别以绝对成功率34%和10%的优势超越模仿学习和强化学习方法,而仅使用一个或两个上下文示例进行提示。
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
2 引言
|
|
||||||
================================================================================
|
|
||||||
人类智能的一个独特特征是能够无缝地将面向任务的行动与语言推理(或内心独白)相结合,这种能力已被理论化为在人类认知中发挥重要作用,能够实现自我调节或策略制定,并维持工作记忆。以厨房烹饪菜肴为例,在任意两个步骤之间,可能会有多种选择:根据冰箱中的内容调整食谱,阅读菜谱描述以了解下一步该做什么,并确保菜肴有新鲜原料和适当调料。这种在推理(用于计划或将目标分解为子目标)与行动(与环境交互)之间进行决策的能力对于人类完成复杂任务至关重要,也被认为是人类认知能力的基础之一。
|
|
||||||
|
|
||||||
最近,人们对开发能够执行行动的自主代理产生了极大兴趣,这些代理能够根据语言描述为现实世界中的决策任务(例如在线购物和家庭家务)生成行动计划并执行它们。尽管这些语言模型在语言和决策任务上展现出令人印象深刻的结果,但它们是在有限的环境中进行研究的,行动空间是简单的,反馈是程序生成的。此外,这些代理没有使用自由形式的语言推理,这限制了它们以类人方式表达、讨论和解决问题以及从交互中学习的能力。
|
|
||||||
|
|
||||||
本文探索了如何将大型语言模型(LLMs)用于推理(特别是思维链能力)和行动,以使两者协同工作:生成推理轨迹使模型能够诱导、跟踪和更新行动计划,以及处理异常情况。虽然LLMs在推理方面取得了显著进展,但主要是在静态推理问题上,如数学推理、常识推理和符号推理。相比之下,我们认为语言和行动之间也存在协同作用,允许模型执行动态推理以构建、维护和调整行动计划,同时与外部环境(例如维基百科)或其他工具进行交互,以收集额外信息,从而在知识密集型推理任务(如问答和事实验证)上获得更好的结果。
|
|
||||||
|
|
||||||
我们将这种推理和行动协同的方法命名为ReAct(Reasoning and Acting)。通过HotpotQA(多跳问答)和Fever(事实验证)实验,我们证明ReAct能够有效克服思维链推理中的幻觉和错误传播问题。具体来说,在HotpotQA上的成功率相对于思维链方法提高了14%,在Fever上达到与思维链加行动方法相当或更好的性能。我们还证明了ReAct相对于纯行动基线方法在可解释性和可信度方面的优势。
|
|
||||||
|
|
||||||
此外,我们还测试ReAct在两个交互式决策任务上的表现:ALFWorld(基于文本的家庭环境)和WebShop(在线购物网站环境)。通过少量示例学习,ReAct在这两个任务上的成功率分别达到34%和10%,显著超越了模仿学习和强化学习的基线方法。总之,我们的贡献包括:(1)引入了ReAct,这是一种新颖的范式,允许大型语言模型交错生成推理轨迹和任务特定行动,实现两者之间更大的协同;(2)通过多个领域的实验证明了ReAct在知识密集型推理任务和交互式决策任务上的有效性;(3)对ReAct与现有方法进行了全面分析,突出了其在可解释性、可信度和学习能力方面的优势。
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
3 方法
|
|
||||||
================================================================================
|
|
||||||
ReAct提示方法的核心思想是将推理和行动交错进行。在知识密集型任务(如HotpotQA和Fever)中,我们使用简单的维基百科API作为行动空间。模型可以执行两种类型的行动:(1)搜索实体,从维基百科返回前k个句子;(2)查找字符串,返回包含该字符串的页面中的所有句子,模拟浏览器中的Ctrl+F功能。
|
|
||||||
|
|
||||||
对于每个任务,我们从训练集中随机选择少量示例,并手动编写ReAct格式的轨迹作为少样本提示的示例。ReAct格式的轨迹包括交替出现的思考(Thought)和行动(Action)。思考是模型对当前状态的分析和推理,而行动是模型决定采取的下一步操作。这种格式允许模型在执行行动之前先进行推理,从而提高任务解决的准确性和可解释性。
|
|
||||||
|
|
||||||
在决策任务(如ALFWorld和WebShop)中,ReAct以类似的方式工作。模型接收环境观察并生成思考和行动。思考帮助模型理解当前环境状态并计划下一步行动,而行动则改变环境状态并产生新的观察。通过这种交互式循环,ReAct能够逐步完成复杂的决策任务。
|
|
||||||
|
|
||||||
ReAct的关键优势在于它将推理和行动紧密结合。推理轨迹不仅帮助模型规划行动,还提供了解释模型行为的基础。这种可解释性对于理解模型决策过程和调试错误非常重要。此外,ReAct的交错生成方式允许模型根据行动的结果动态调整推理,从而实现更灵活的任务解决策略。
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
4 相关工作
|
|
||||||
================================================================================
|
|
||||||
大型语言模型用于推理的工作中最著名的是思维链(Chain-of-Thought,CoT),它揭示了大型语言模型制定推理步骤的能力。后续工作包括自一致性(Self-Consistency)、思维树(Tree of Thoughts)等,这些方法主要关注如何提高推理的准确性和效率。
|
|
||||||
|
|
||||||
在语言模型用于行动方面,先前的工作包括WebGPT,它使用网络搜索来回答开放域问题,以及SayCan等机器人系统,它们将自然语言指令转换为机器人行动。这些方法主要关注如何使模型能够在特定环境中执行行动。
|
|
||||||
|
|
||||||
ReAct的独特之处在于它将推理和行动相结合,而不是将它们作为独立的过程。这种协同方法允许模型在执行行动之前进行推理,并根据行动结果更新推理。这与人类认知过程更加相似,也使模型更具可解释性和可信度。
|
|
||||||
|
|
||||||
我们的工作也与工具增强语言模型相关,例如Toolformer和Gorilla。这些模型被训练或微调以使用外部工具(如API、搜索引擎等)。ReAct采用了类似的思想,但强调推理和行动的交错进行,而不是简单地调用工具。
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
5 结论
|
|
||||||
================================================================================
|
|
||||||
本文提出了ReAct,一种将推理和行动协同应用于大型语言模型的新范式。通过在各种语言和决策任务上的实验,我们证明了ReAct的有效性。ReAct通过交错生成推理轨迹和任务特定行动,实现了推理和行动之间的更大协同,从而提高了任务的准确性和可解释性。
|
|
||||||
|
|
||||||
ReAct的优势包括:(1)克服思维链推理中的幻觉和错误传播问题;(2)提供更具可解释性和可信度的任务解决轨迹;(3)在交互式决策任务上显著超越传统的模仿学习和强化学习方法;(4)只需少量示例即可实现良好的性能。
|
|
||||||
|
|
||||||
未来的研究方向包括:扩展ReAct到更多样化的任务和环境、探索更高效的提示策略、以及将ReAct的思想应用于模型微调。我们相信ReAct为开发更智能、更可解释的语言模型代理提供了一个有前途的方向。
|
|
||||||
Loading…
Reference in New Issue
Block a user