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:
JOJO 2026-06-01 13:17:32 +08:00
parent be08026fca
commit eaa5e3bee9
49 changed files with 633 additions and 22149 deletions

15
.gitignore vendored
View File

@ -12,17 +12,17 @@ __pycache__/
venv/
# Runtime data (main agent)
# 注意:运行态数据默认已迁出源码树到 ~/.agents/<mode>/。
# 以下条目用于忽略「通过具体目录变量指回源码树」或历史遗留的运行态目录。
logs/
data/
project/
users/
api/
webapp.pid
# Runtime data (sub agent)
sub_agent/tasks/
sub_agent/data/
sub_agent/logs/
sub_agent/project/
# 旧版 sub_agent/ 子项目已移除,子智能体逻辑现统一在 easyagent/。
sub_agent_results/
# Misc
@ -33,10 +33,11 @@ static/dist/
*.tsbuildinfo
user_upload/
skills/
ax_probe_chrome.py
# Ignore docs and runtime pid
webapp.pid
# 本地实验残留与历史文档归档(不进仓库)
_experiments/
# Ignore docs
doc/
# Host workspace config (local, use .example)

View File

View File

@ -31,7 +31,45 @@
- `docs/cli_slash_commands_spec.md`: CLI `/` 指令设计说明
- **其他子项目/资源**
- `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) 当前可用启动/构建命令(已按代码核对)
@ -163,7 +201,8 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
## 7) 安全与仓库卫生
- 严禁提交真实密钥(`.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/`)纳入提交。
## 8) Android App 发布联动要求(重要)

View File

@ -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 测试功能。

View File

@ -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接近流式效果
- ✅ 刷新后不重复显示内容
- ✅ 任务完成后不重复加载历史
- ✅ 状态检查避免重复恢复
现在可以启动服务器测试了!

View File

@ -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 轮询)
现在可以测试了!刷新页面应该只显示一次内容,新内容会正常追加。

View File

@ -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 轮询)
- ✅ 不重复显示内容
现在可以测试了!刷新页面后,后端应该继续执行,前端会正常恢复并显示新内容。

View File

@ -121,11 +121,12 @@ npm --prefix cli run build
# 查看容器状态
docker ps -a | grep agent-term
# 查看调试日志
tail -f logs/debug_stream.log
# 查看调试日志(运行态日志默认在 ~/.agents/<mode>/logs/
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
@ -149,6 +150,24 @@ tail -f logs/container_stats.log
- `PROJECT_MAX_STORAGE_MB` - 单用户磁盘配额 (默认 2048MB)
- `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
### File Operations
@ -281,20 +300,29 @@ agents/
├── server/ # Flask 业务主线chat/task/goal/monitor/auth ...
├── core/ # 核心终端与工具编排main_terminal_parts/*
├── modules/ # 独立功能模块
├── utils/ # 辅助工具
├── utils/ # 辅助工具(含 log_rotation 日志轮转)
├── config/ # 配置拆分api/limits/terminal/paths ...,由 __init__ 聚合)
├── static/ # Vue 3 + TS Web 前端
├── cli/ # React + Ink CLI 前端(重写中)
├── easyagent/ # 子智能体执行逻辑(旧版 sub_agent/ 已删除)
├── docker/ # 容器镜像
├── prompts/ # 系统提示词
├── scripts/ # 运维脚本(数据迁移 migrate_runtime_data.py 等)
├── android-webview-app/ # Android WebView 客户端工程
├── test/ # 测试用例
├── docs/ / doc/ # 设计与发布文档
├── users/ # 用户工作区运行态gitignored
├── data/ # 全局数据运行态gitignored
└── logs/ # 日志文件运行态gitignored
└── _experiments/ # 本地实验残留与历史文档归档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
- 修改系统提示词:编辑 `prompts/main_system.txt`

View File

@ -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.

View File

@ -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
## 总结
核心功能已经实现完成,包括:
- ✅ 批处理模式
- ✅ 工具定义和执行
- ✅ 子智能体管理
- ✅ 对话记录保存
- ✅ 统计信息收集
- ✅ 兜底机制
还需要完成:
- ⏳ 后台任务轮询
- ⏳ 对话状态管理
- ⏳ 前端集成
- ⏳ 测试和调试
整体架构清晰,代码结构合理,可以开始测试和完善了。

View File

@ -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. 测试完整流程

View File

@ -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. **对话记录保留**:便于调试和审计

View File

@ -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. 完善文档和示例

View File

@ -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>
```
## 用户最新的一次输入
依然有,我用的无痕模式都有,和之前一模一样

View File

@ -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_modelshost_workspacesauto_approval 属于
配置而非运行态数据仍锚定在源码树内不随运行态根目录迁移
"""
import os
from pathlib import Path
@ -7,40 +27,87 @@ from pathlib import 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()
def _norm(path: Path) -> Path:
"""展开 ``~``、转绝对路径并归一化。相对路径相对仓库根目录展开。"""
path = path.expanduser()
if not path.is_absolute():
path = (_REPO_ROOT / path).resolve()
else:
path = path.resolve()
return str(path)
path = _REPO_ROOT / path
return path.resolve()
# 默认项目路径,可通过环境变量覆盖以指向宿主机任意目录
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")
# 宿主机模式工作区配置文件JSON
HOST_WORKSPACES_FILE = _resolve_repo_path(os.environ.get("HOST_WORKSPACES_FILE", ""), "./config/host_workspaces.json")
# 兼容旧配置:若仍有模块读取 HOST_PROJECT_PATH保留该键实际宿主机路径选择改由 JSON 管理)
# 兼容旧配置:若仍有模块读取 HOST_PROJECT_PATH保留该键实际宿主机路径选择改由 JSON 管理)。
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")
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")
WORKSPACE_SKILLS_DIRNAME = ".agents/skills"
# 宿主机模式工作区配置文件JSON属于配置锚定源码树
HOST_WORKSPACES_FILE = _resolve_repo_path(os.environ.get("HOST_WORKSPACES_FILE", ""), "./config/host_workspaces.json")
# 多用户空间
USER_SPACE_DIR = _resolve_repo_path(os.environ.get("USER_SPACE_DIR", ""), "./users")
# ── 基于 DATA_DIR 派生的数据文件 ──
USERS_DB_FILE = f"{DATA_DIR}/users.json"
INVITE_CODES_FILE = f"{DATA_DIR}/invite_codes.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_TOKENS_FILE = f"{DATA_DIR}/api_tokens.json"
API_USAGE_FILE = f"{DATA_DIR}/api_usage.json"
__all__ = [
"RUNTIME_ROOT",
"DEFAULT_PROJECT_PATH",
"HOST_WORKSPACES_FILE",
"HOST_PROJECT_PATH",

View File

@ -1,20 +1,8 @@
"""子智能体相关配置。"""
import os
from pathlib import 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)
from .paths import _resolve_repo_path, DATA_DIR, DEFAULT_PROJECT_PATH
# 子智能体服务
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_TASKS_BASE_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_TASKS_BASE_DIR", ""), "./sub_agent/tasks")
SUB_AGENT_PROJECT_RESULTS_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_PROJECT_RESULTS_DIR", ""), "./project/sub_agent_results")
SUB_AGENT_STATE_FILE = _resolve_repo_path(os.environ.get("SUB_AGENT_STATE_FILE", ""), "./data/sub_agents.json")
# 子智能体任务目录与状态文件属于运行态数据,跟随 DATA_DIR默认 ~/.agents/<mode>/data
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", ""), 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"))
__all__ = [

View File

@ -1,339 +0,0 @@
# AI 编码 Agent 自主工作循环调研报告
> **一句话总结:`/goal` 命令真实存在。** Claude Codev2.1.139, 2026年5月12日发布和 OpenAI Codex CLIv0.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. 被阻塞时如何报告
**来源:**
- 官方 Cookbookhttps://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 messageevaluator 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 modelHaiku | 主工作模型 + 硬编码规则 |
| 判定依据 | 对话 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 workshttps://code.claude.com/docs/en/goal
- 社区文章详解 evaluator loophttps://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/TaskUpdatehttps://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 designedhttps://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 系统**:如果配置了 skillsskill 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 Modeplan 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 Modehttps://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 仓库或官方博客;标注为"社区/非官方"的来源于第三方博客或分析文章。未找到的信息已明确标注"未找到"。

View File

@ -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())

View File

@ -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())

View File

@ -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

View File

@ -1,2 +0,0 @@
hello
world

View File

@ -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())

View File

@ -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()

View 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

View 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())

View File

@ -7,6 +7,7 @@ from pathlib import Path
from typing import Any, Dict, List
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:
file_path.parent.mkdir(parents=True, exist_ok=True)
with file_path.open('a', encoding='utf-8') as f:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
f.write(f"[{timestamp}] {message}\n")
_append_rotating(file_path, f"[{timestamp}] {message}")
def debug_log(message):

View File

@ -18,17 +18,28 @@ class ConfigPathsResolutionTest(unittest.TestCase):
import json
import config
print(json.dumps({
"RUNTIME_ROOT": config.RUNTIME_ROOT,
"DEFAULT_PROJECT_PATH": config.DEFAULT_PROJECT_PATH,
"HOST_WORKSPACES_FILE": config.HOST_WORKSPACES_FILE,
"PROMPTS_DIR": config.PROMPTS_DIR,
"DATA_DIR": config.DATA_DIR,
"LOGS_DIR": config.LOGS_DIR,
"USER_SPACE_DIR": config.USER_SPACE_DIR,
"API_USER_SPACE_DIR": config.API_USER_SPACE_DIR,
"SUB_AGENT_STATE_FILE": config.SUB_AGENT_STATE_FILE,
"SUB_AGENT_TASKS_BASE_DIR": config.SUB_AGENT_TASKS_BASE_DIR,
}, ensure_ascii=False))
"""
env = dict(os.environ)
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:
env.update(extra_env)
completed = subprocess.run(
@ -43,36 +54,98 @@ print(json.dumps({
self.assertTrue(lines, f"stdout is empty, stderr={completed.stderr}")
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:
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():
value = Path(str(raw))
self.assertTrue(value.is_absolute(), f"{key} is not absolute: {raw}")
web_home = Path("~/.agents/web").expanduser().resolve()
self.assertEqual(data["RUNTIME_ROOT"], str(web_home))
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(
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:
data = self._load_paths(
cwd=Path(td),
extra_env={
"DEFAULT_PROJECT_PATH": "./workspace_rel",
"TERMINAL_SANDBOX_MODE": "docker",
"DATA_DIR": "./data_rel",
"LOGS_DIR": "./logs_rel",
"USER_SPACE_DIR": "./users_rel",
"DEFAULT_PROJECT_PATH": "./workspace_rel",
"HOST_WORKSPACES_FILE": "./config/host_workspaces_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["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["DEFAULT_PROJECT_PATH"], str((self.repo_root / "workspace_rel").resolve()))
self.assertEqual(
data["HOST_WORKSPACES_FILE"],
str((self.repo_root / "config" / "host_workspaces_rel.json").resolve()),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -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)

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -21,7 +21,8 @@ try:
DEFAULT_RESPONSE_MAX_TOKENS,
THINKING_API_BASE_URL,
THINKING_API_KEY,
THINKING_MODEL_ID
THINKING_MODEL_ID,
LOGS_DIR,
)
except ImportError:
import sys
@ -37,9 +38,17 @@ except ImportError:
DEFAULT_RESPONSE_MAX_TOKENS,
THINKING_API_BASE_URL,
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
def __init__(self, thinking_mode: bool = True, web_mode: bool = False):
self.fast_api_config = {
@ -79,20 +88,19 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
self.last_call_used_thinking = False # 最近一次调用是否使用思考模型
# 最近一次API错误详情
self.last_error_info: Optional[Dict[str, Any]] = None
# 请求体落盘目录
self.request_dump_dir = Path(__file__).resolve().parents[1] / "logs" / "api_requests"
self.request_dump_dir.mkdir(parents=True, exist_ok=True)
self.debug_log_path = Path(__file__).resolve().parents[1] / "logs" / "api_debug.log"
# 请求体落盘目录(跟随 LOGS_DIR默认 ~/.agents/<mode>/logs/api_requests
self.request_dump_dir = Path(LOGS_DIR) / "api_requests"
self.debug_log_path = Path(LOGS_DIR) / "api_debug.log"
def _debug_log(self, payload: Dict[str, Any]) -> None:
if not _api_dump_enabled():
return
try:
entry = {
"ts": datetime.now().isoformat(),
**payload
}
self.debug_log_path.parent.mkdir(parents=True, exist_ok=True)
with self.debug_log_path.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
append_line(self.debug_log_path, json.dumps(entry, ensure_ascii=False))
except Exception:
pass
@ -482,15 +490,18 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"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]:
"""
将本次请求的payloadheaders配置落盘便于排查400等错误
返回写入的文件路径
返回写入的文件路径落盘开关关闭默认时返回 None
"""
if not _api_dump_enabled():
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
filename = f"req_{timestamp}.json"
path = self.request_dump_dir / filename
try:
self.request_dump_dir.mkdir(parents=True, exist_ok=True)
headers_sanitized = {}
for k, v in headers.items():
headers_sanitized[k] = "***" if k.lower() == "authorization" else v
@ -501,6 +512,8 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"payload": payload
}
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:
self._print(f"{OUTPUT_FORMATS['warning']} 请求体落盘失败: {exc}")
return path
@ -783,7 +796,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"error_text": error_text,
"error_type": 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"),
"model_id": api_config.get("model_id"),
"model_key": self.model_key
@ -803,7 +816,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"),
"model_key": self.model_key,
"request_dump": str(dump_path)
"request_dump": (str(dump_path) if dump_path else None)
})
self._print(
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_type": 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"),
"model_id": api_config.get("model_id"),
"model_key": self.model_key
@ -857,7 +870,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"),
"model_key": self.model_key,
"request_dump": str(dump_path)
"request_dump": (str(dump_path) if dump_path else None)
})
self._print(
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_message": f"无法连接到API服务器: {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"),
"model_id": api_config.get("model_id"),
"model_key": self.model_key
@ -895,7 +908,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"),
"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}")
yield {"error": self.last_error_info}
@ -906,7 +919,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"error_text": "timeout",
"error_type": "timeout",
"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"),
"model_id": api_config.get("model_id"),
"model_key": self.model_key
@ -918,7 +931,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"),
"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")
yield {"error": self.last_error_info}
@ -930,7 +943,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"error_text": error_text,
"error_type": "exception",
"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"),
"model_id": api_config.get("model_id"),
"model_key": self.model_key
@ -942,7 +955,7 @@ class DeepSeekClient: # legacy name; generic OpenAI-compatible client
"base_url": api_config.get("base_url"),
"model_id": api_config.get("model_id"),
"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)
yield {"error": self.last_error_info}

View File

@ -3,30 +3,25 @@
from __future__ import annotations
import json
import threading
from datetime import datetime
from pathlib import Path
from typing import Any
from config import LOGS_DIR
from utils.log_rotation import append_line
_LOG_FILE = Path(LOGS_DIR).expanduser().resolve() / "host_workspace_debug.log"
_LOCK = threading.Lock()
def write_host_workspace_debug(event: str, **payload: Any) -> None:
"""写入一条宿主机工作区调试日志"""
"""写入一条宿主机工作区调试日志(超过阈值自动轮转)"""
try:
record = {
"ts": datetime.now().isoformat(),
"event": event,
**payload,
}
line = 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")
append_line(_LOG_FILE, json.dumps(record, ensure_ascii=False, default=str))
except Exception:
# 调试日志不应影响主流程
return

127
utils/log_rotation.py Normal file
View File

@ -0,0 +1,127 @@
"""日志与落盘文件的轮转/清理工具。
提供两类策略
1. ``RotatingFileHandler`` 风格的"按大小轮转"用于持续追加的单文件日志
host_workspace_debug.logchunk_*.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

View File

@ -3,6 +3,7 @@
import logging
import os
from datetime import datetime
from logging.handlers import RotatingFileHandler
from pathlib import Path
try:
from config import LOGS_DIR, LOG_LEVEL, LOG_FORMAT
@ -14,6 +15,8 @@ except ImportError:
sys.path.insert(0, str(project_root))
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:
value = str(os.environ.get(name, "")).strip().lower()
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_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.setFormatter(formatter)
logger.addHandler(file_handler)

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -1 +0,0 @@
3120088

View File

@ -1,14 +0,0 @@
● webapp.service - Web Application Service
Loaded: loaded (]8;;file://iZ2ze5i9ndpg9j4i6l1sfdZ/etc/systemd/system/webapp.service/etc/systemd/system/webapp.service]8;;; enabled; preset: enabled)
Active: active (running) 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
└─392156 python3 web_server.py
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.

View File

@ -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 实时堆叠恢复正常,刷新与实时显示一致。

View File

@ -1,24 +0,0 @@
# 诡异Bug记录
- 诡异程度:★★★★☆
- 发现时间2026-02-25 13:21:48
- 场景/模块:前端聊天流式渲染(块与输出内容错位)
- 关联模型minimax-m2.5
## 描述
使用 minimax 模型时出现“块与输出内容错位”的严重前端问题,表现为流式阶段内容与对应块不对齐。
## Debug 过程
1. 回顾近期针对 minimax 的堆叠修复(延迟创建 text action
2. 复现后确认错位仅发生在该修复逻辑开启时。
3. 临时回退到方案1text_start 直接创建 text action + 堆叠时忽略空文本 action进行验证。
## 找到的问题
延迟创建 text action 的策略会在部分 minimax 流式节奏下打乱 action 与 chunk 的对应关系,导致内容落入错误块中。
## 修复方案
改回方案1
- text_start 直接创建 text action
- 堆叠渲染时忽略空白 text action。
结果:错位问题消失。

View File

@ -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

View File

@ -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 dont 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 models
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, · · · , ot1, at1, 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 agents 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 ReActs 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, IMs “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.

View File

@ -1,50 +0,0 @@
【翻译后 - 中文译文】
================================================================================
1 摘要
================================================================================
大型语言模型LLMs在语言理解和交互式决策等任务上展现出卓越的性能但其推理能力如思维链提示和行动能力如行动计划生成主要作为独立主题进行研究。本文探索利用大型语言模型以交错方式生成推理轨迹和任务特定行动使两者之间产生更大协同效应推理轨迹帮助模型诱导、跟踪和更新行动计划以及处理异常情况而行动则使其能够与外部来源如知识库或环境进行交互并收集额外信息。我们将这种名为ReAct的方法应用于多样化的语言和决策任务证明其相对于最先进基线方法的有效性同时提高了人类可解释性和可信度。具体而言在问答任务HotpotQA和事实验证任务FeverReAct通过与简单的维基百科API交互克服了思维链推理中普遍存在的幻觉和错误传播问题并生成比没有推理轨迹的基线方法更具可解释性的类人任务解决轨迹。此外在两个交互式决策基准测试ALFWorld和WebShopReAct分别以绝对成功率34%和10%的优势超越模仿学习和强化学习方法,而仅使用一个或两个上下文示例进行提示。
================================================================================
2 引言
================================================================================
人类智能的一个独特特征是能够无缝地将面向任务的行动与语言推理(或内心独白)相结合,这种能力已被理论化为在人类认知中发挥重要作用,能够实现自我调节或策略制定,并维持工作记忆。以厨房烹饪菜肴为例,在任意两个步骤之间,可能会有多种选择:根据冰箱中的内容调整食谱,阅读菜谱描述以了解下一步该做什么,并确保菜肴有新鲜原料和适当调料。这种在推理(用于计划或将目标分解为子目标)与行动(与环境交互)之间进行决策的能力对于人类完成复杂任务至关重要,也被认为是人类认知能力的基础之一。
最近,人们对开发能够执行行动的自主代理产生了极大兴趣,这些代理能够根据语言描述为现实世界中的决策任务(例如在线购物和家庭家务)生成行动计划并执行它们。尽管这些语言模型在语言和决策任务上展现出令人印象深刻的结果,但它们是在有限的环境中进行研究的,行动空间是简单的,反馈是程序生成的。此外,这些代理没有使用自由形式的语言推理,这限制了它们以类人方式表达、讨论和解决问题以及从交互中学习的能力。
本文探索了如何将大型语言模型LLMs用于推理特别是思维链能力和行动以使两者协同工作生成推理轨迹使模型能够诱导、跟踪和更新行动计划以及处理异常情况。虽然LLMs在推理方面取得了显著进展但主要是在静态推理问题上如数学推理、常识推理和符号推理。相比之下我们认为语言和行动之间也存在协同作用允许模型执行动态推理以构建、维护和调整行动计划同时与外部环境例如维基百科或其他工具进行交互以收集额外信息从而在知识密集型推理任务如问答和事实验证上获得更好的结果。
我们将这种推理和行动协同的方法命名为ReActReasoning 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和WebShopReAct以类似的方式工作。模型接收环境观察并生成思考和行动。思考帮助模型理解当前环境状态并计划下一步行动而行动则改变环境状态并产生新的观察。通过这种交互式循环ReAct能够逐步完成复杂的决策任务。
ReAct的关键优势在于它将推理和行动紧密结合。推理轨迹不仅帮助模型规划行动还提供了解释模型行为的基础。这种可解释性对于理解模型决策过程和调试错误非常重要。此外ReAct的交错生成方式允许模型根据行动的结果动态调整推理从而实现更灵活的任务解决策略。
================================================================================
4 相关工作
================================================================================
大型语言模型用于推理的工作中最著名的是思维链Chain-of-ThoughtCoT它揭示了大型语言模型制定推理步骤的能力。后续工作包括自一致性Self-Consistency、思维树Tree of Thoughts这些方法主要关注如何提高推理的准确性和效率。
在语言模型用于行动方面先前的工作包括WebGPT它使用网络搜索来回答开放域问题以及SayCan等机器人系统它们将自然语言指令转换为机器人行动。这些方法主要关注如何使模型能够在特定环境中执行行动。
ReAct的独特之处在于它将推理和行动相结合而不是将它们作为独立的过程。这种协同方法允许模型在执行行动之前进行推理并根据行动结果更新推理。这与人类认知过程更加相似也使模型更具可解释性和可信度。
我们的工作也与工具增强语言模型相关例如Toolformer和Gorilla。这些模型被训练或微调以使用外部工具如API、搜索引擎等。ReAct采用了类似的思想但强调推理和行动的交错进行而不是简单地调用工具。
================================================================================
5 结论
================================================================================
本文提出了ReAct一种将推理和行动协同应用于大型语言模型的新范式。通过在各种语言和决策任务上的实验我们证明了ReAct的有效性。ReAct通过交错生成推理轨迹和任务特定行动实现了推理和行动之间的更大协同从而提高了任务的准确性和可解释性。
ReAct的优势包括1克服思维链推理中的幻觉和错误传播问题2提供更具可解释性和可信度的任务解决轨迹3在交互式决策任务上显著超越传统的模仿学习和强化学习方法4只需少量示例即可实现良好的性能。
未来的研究方向包括扩展ReAct到更多样化的任务和环境、探索更高效的提示策略、以及将ReAct的思想应用于模型微调。我们相信ReAct为开发更智能、更可解释的语言模型代理提供了一个有前途的方向。