Improve long product-data generation runs

This commit is contained in:
wuyang6
2026-05-11 20:26:06 +08:00
parent 4c29ce21f1
commit c4427bbd07
14 changed files with 57 additions and 6 deletions
+2
View File
@@ -589,6 +589,7 @@ bash "$HOME/zk-data-agent/scripts/install-from-git.sh"
OPENAI_API_KEY
OPENAI_BASE_URL
OPENAI_MODEL
OPENAI_TIMEOUT_SECONDS
CLAW_BACKEND_HOST
CLAW_BACKEND_PORT
CLAW_FRONTEND_HOST
@@ -730,6 +731,7 @@ cat .env.deploy
- `OPENAI_API_KEY`
- `OPENAI_BASE_URL`
- `OPENAI_MODEL`
- `OPENAI_TIMEOUT_SECONDS`
然后看后端日志:
+7
View File
@@ -278,6 +278,7 @@ class AgentInstanceConfig:
model: str
base_url: str
api_key: str
timeout_seconds: float
allow_shell: bool
allow_write: bool
@@ -483,6 +484,7 @@ class AgentState:
model: str,
base_url: str,
api_key: str,
timeout_seconds: float,
allow_shell: bool,
allow_write: bool,
session_directory: Path,
@@ -498,6 +500,7 @@ class AgentState:
model=model,
base_url=base_url,
api_key=api_key,
timeout_seconds=timeout_seconds,
allow_shell=allow_shell,
allow_write=allow_write,
)
@@ -772,6 +775,7 @@ class AgentState:
model=config.model,
base_url=config.base_url,
api_key=config.api_key,
timeout_seconds=config.timeout_seconds,
)
return LocalCodingAgent(
model_config=model_config,
@@ -1753,6 +1757,9 @@ def _interrupted_session_message(status: str) -> str:
def _runtime_event_stage(event: dict[str, object]) -> str:
event_type = event.get('type')
if event_type == 'tool_start':
stage_note = str(event.get('assistant_content') or '').strip()
if stage_note.startswith(('进度:', '进度:')):
return stage_note
tool_name = str(event.get('tool_name') or '').strip()
return f'调用工具 {tool_name}' if tool_name else '正在调用工具'
if event_type == 'tool_result':
+1 -1
View File
@@ -15,7 +15,7 @@ import {
} from "@/lib/claw-auth";
export const runtime = "nodejs";
export const maxDuration = 300;
export const maxDuration = 3600;
type ClawChatResponse = {
final_output?: string;
@@ -566,8 +566,16 @@ function summarizeRunEvents(runStatus: ClawActiveRunStatus) {
lines.push("后端已开始执行本轮任务。");
}
if (event.type === "tool_start") {
const stageNote =
typeof event.assistant_content === "string"
? event.assistant_content.trim()
: "";
lines.push(
event.tool_name ? `调用工具 ${event.tool_name}` : "正在调用工具",
stageNote.startsWith("进度:") || stageNote.startsWith("进度:")
? stageNote
: event.tool_name
? `调用工具 ${event.tool_name}`
: "正在调用工具",
);
}
if (event.type === "tool_result") {
@@ -636,7 +644,6 @@ function getReplayMessages(
const visibleMessages = isActiveRunStatus(runStatus)
? messages.filter((message) => !isRunStatusMessage(message))
: [...messages];
if (isActiveRunStatus(runStatus)) return visibleMessages;
return trimIncompleteReplayTail(visibleMessages);
}
@@ -1601,6 +1601,17 @@ function formatActivityDuration(ms: number) {
function compactActivityLabel(label: string, maxLength = 15) {
const normalized = label.replace(/\s+/g, " ").trim();
const progress = normalized.match(
/^进度[:]\s*批\s*(\d+)\s*\/\s*(\d+)(?:[,]\s*已生成\s*(\d+)\s*\/\s*(\d+))?/,
);
if (progress) {
const batch = `${progress[1]}/${progress[2]}`;
const count =
progress[3] && progress[4] ? `${progress[3]}/${progress[4]}` : "";
const compactProgress = `${batch}${count}`;
if (compactProgress.length <= maxLength) return compactProgress;
return batch;
}
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, Math.max(1, maxLength - 1))}`;
}
+2
View File
@@ -161,6 +161,7 @@ write_env_file() {
export OPENAI_API_KEY=$(printf "%q" "${api_key}")
export OPENAI_BASE_URL=$(printf "%q" "${base_url}")
export OPENAI_MODEL=$(printf "%q" "${model}")
export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}"
export CLAW_DEPLOY_INSTANCE=$(printf "%q" "${DEPLOY_INSTANCE}")
export CLAW_BACKEND_HOST=$(printf "%q" "${backend_host}")
@@ -184,6 +185,7 @@ ensure_env_file() {
echo "CLAW_DEPLOY_INSTANCE=${DEPLOY_INSTANCE}"
echo "OPENAI_BASE_URL=${OPENAI_BASE_URL:-}"
echo "OPENAI_MODEL=${OPENAI_MODEL:-}"
echo "OPENAI_TIMEOUT_SECONDS=${OPENAI_TIMEOUT_SECONDS:-3600}"
echo "OPENAI_API_KEY=已配置"
return
fi
+2
View File
@@ -19,6 +19,7 @@ fi
export OPENAI_API_KEY
export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}"
export OPENAI_MODEL="${OPENAI_MODEL:-xiaomi/mimo-v2-flash}"
export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}"
# 后端也会调用 Node 生态工具,例如 feishu-mcp-pro。systemd 不读取交互式
# shell 的 nvm 配置,所以需要把部署时记录的 Node 路径显式传给 Python 进程。
@@ -38,6 +39,7 @@ exec "${PYTHON_BIN}" -m src.gui \
--host "${BACKEND_HOST}" \
--port "${BACKEND_PORT}" \
--cwd "${ROOT_DIR}" \
--timeout-seconds "${OPENAI_TIMEOUT_SECONDS}" \
--session-dir "${ROOT_DIR}/.port_sessions/agent" \
--allow-write \
--allow-shell \
+2
View File
@@ -141,6 +141,7 @@ fi
export OPENAI_API_KEY="${OPENAI_API_KEY:-}"
export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}"
export OPENAI_MODEL="${OPENAI_MODEL:-xiaomi/mimo-v2-flash}"
export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}"
BACKEND_LOG="${RUN_DIR}/webui-backend.log"
FRONTEND_LOG="${RUN_DIR}/webui-frontend.log"
@@ -151,6 +152,7 @@ start_detached_process "backend" "${RUN_DIR}/webui-backend.pid" "${BACKEND_LOG}"
--host "${BACKEND_HOST}" \
--port "${BACKEND_PORT}" \
--cwd "${ROOT_DIR}" \
--timeout-seconds "${OPENAI_TIMEOUT_SECONDS}" \
--session-dir "${RUN_DIR}/agent" \
--allow-write \
--allow-shell \
+7 -1
View File
@@ -66,4 +66,10 @@ cat input.json | python skills/product-data/scripts/<tool>.py
最后从 `scratchpad/normalized_records.jsonl` 统一校验和导出。
为避免长时间连续生成导致模型请求超时,单轮最多连续生成 3 批;超过 24 条时先阶段性停顿,用户回复“继续生成”后先统计 `scratchpad/normalized_records.jsonl` 已有行数,再从下一批 append。
用户确认计划后,生成任务应在同一轮内尽量跑完整个批次链路;不要因为超过 24 条就主动停顿等待继续。每批工具调用前用固定格式输出进度,方便 Web 摘要行展示:
```text
进度:批 i/n,已生成 x/y,主题
```
如果确实因为超时、取消或工具错误中断,用户回复继续后先统计 `scratchpad/normalized_records.jsonl` 已有行数,再从下一批 append。
+3 -2
View File
@@ -171,7 +171,7 @@ review 展示必须简短清晰,不要重复解释工具和流程。每次 rev
11. 分批生成 dataset draft text v1,每批最多 8 条,不要用 `write_file` 保存 `draft_part*.txt` 这类中间草稿。
12. 如果是继续一个已确认但中断/超时的生成任务,先检查 `scratchpad/normalized_records.jsonl` 是否已存在;存在时先用 `python_exec` 统计已有记录数,然后从下一批继续追加,禁止覆盖已有记录。
13. 每生成一批,立刻使用 `python_exec``script_path` 模式执行 `skills/product-data/scripts/normalize_dataset_draft.py`:通过 `stdin` 传入小批量 JSON,必须带 `confirmed_plan_id``records_output_path="scratchpad/normalized_records.jsonl"``append=true``return_records=false`draft 中每条 case 都必须有 `complex: true/false`
14. 单轮最多连续生成并 normalize 3 批;如果计划还没完成,先停止并汇报“已生成 X/Y 条,回复继续生成可从下一批继续”,不要在同一轮里继续生成第 4 批
14. 批次数超过 1 时,每批工具调用前必须用一句阶段说明标记进度,格式固定为:`进度:批 i/n,已生成 x/y,主题`。例如:`进度:批 3/7,已生成 16/50,路线偏好追加`。摘要行会优先展示这个格式
15. 所有批次 normalize 完成后,使用 `python_exec` 执行 `skills/product-data/scripts/validate_dataset_records.py`,传 `records_path="scratchpad/normalized_records.jsonl"`
16. 如果用户要求落盘 canonical records,使用 `python_exec` 执行 `skills/product-data/scripts/export_dataset_records.py`,逻辑输出固定为 `output/records.jsonl`,默认导出紧凑 JSONL,并同时生成同目录 `records.csv` 表格,不要用 `write_file` 手写 JSON 或 CSV。
17. 如果用户已经明确要训练数据,使用 `python_exec` 执行 `skills/product-data/scripts/export_training_jsonl.py`,优先传 `records_path`,输出固定为 `output/training.jsonl`
@@ -181,7 +181,8 @@ review 展示必须简短清晰,不要重复解释工具和流程。每次 rev
生成阶段要避免一次性把大量数据塞进工具参数:
- 单次 `normalize_dataset_draft.py` 最多处理 8 条 case;计划数量更多时,分批生成、分批 normalize 到同一个 `scratchpad/normalized_records.jsonl`,再统一校验和导出。
- 单轮最多推进 3 批,超过 24 条的数据要做阶段性停顿,等待用户回复“继续生成”再继续;这是为了避免模型长时间连续生成导致请求超时
- 不要因为批次数多就中途停止等待用户继续;用户确认计划后,除非遇到工具错误、模型错误、用户取消或标签/边界歧义,否则在同一轮里把所有批次生成、校验和导出跑完
- 每个批次工具调用前的自然语言进度必须使用 `进度:批 i/n,已生成 x/y,主题`,让 Web 摘要行能展示当前进度。
- 继续生成前必须先统计 `scratchpad/normalized_records.jsonl` 的现有行数;如果已有记录,后续 normalize 必须 `append=true`,不要重新从第 1 批覆盖。
- dataset draft 中的 Agent target 推荐写成单引号形式,例如 `target: Agent(tag='地图导航')`。工具会规范化为 `Agent(tag="地图导航")`,这样可以降低 tool call JSON 里双引号转义失败的概率。
- `complex:` 独立写一行,不要写进 `target:`;工具会在导出训练数据和流转表格时自动组合成 `complex=false\nAgent(...)`
+8
View File
@@ -38,6 +38,12 @@ def main() -> None:
'--api-key',
default=os.environ.get('OPENAI_API_KEY', ''),
)
parser.add_argument(
'--timeout-seconds',
type=float,
default=float(os.environ.get('OPENAI_TIMEOUT_SECONDS', '3600')),
help='Timeout for each model API request in seconds.',
)
parser.add_argument(
'--session-dir',
default=str(DEFAULT_AGENT_SESSION_DIR),
@@ -58,6 +64,7 @@ def main() -> None:
model=args.model,
base_url=args.base_url,
api_key=args.api_key,
timeout_seconds=args.timeout_seconds,
allow_shell=args.allow_shell,
allow_write=args.allow_write,
session_directory=Path(args.session_dir),
@@ -69,6 +76,7 @@ def main() -> None:
print(f' cwd : {state.cwd}')
print(f' model : {state.model}')
print(f' base-url : {state.base_url}')
print(f' timeout : {args.timeout_seconds:g}s')
print(f' sessions : {state.session_directory}')
print(f' shell : {"on" if state.allow_shell else "off"}')
print(f' write : {"on" if state.allow_write else "off"}')
+1
View File
@@ -20,6 +20,7 @@ def _build_client(tmp: Path) -> tuple[TestClient, AgentState]:
model='test-model',
base_url='http://127.0.0.1:8000/v1',
api_key='local-token',
timeout_seconds=120.0,
allow_shell=False,
allow_write=False,
session_directory=tmp / 'sessions',
+1
View File
@@ -37,6 +37,7 @@ def _build_client(tmp: Path) -> tuple[TestClient, AgentState]:
model='test-model',
base_url='http://127.0.0.1:8000/v1',
api_key='local-token',
timeout_seconds=120.0,
allow_shell=False,
allow_write=False,
session_directory=tmp / 'sessions',
+1
View File
@@ -115,6 +115,7 @@ class ModelListTests(unittest.TestCase):
model='xiaomi/mimo-v2-flash',
base_url='http://model.example/v1',
api_key='token',
timeout_seconds=120.0,
allow_shell=False,
allow_write=False,
session_directory=Path(tmp_dir) / 'sessions',