refactor(prompt): 精简 system prompt,媒体段改按需注入,通用任务每轮瘦 ~40 行

去 system prompt 冗余 + 让无关段不常驻:
- 「宪法」文件命名约定 ~25→~6 行:只留格式定义+注入值+一行 current/重定调;
  操作细节本就由 proposal/ppt skill 各自讲,两 skill 引用不动也不破
- run_python「先 write script 再 script_path」去重:模板+agent_builder 两处合一,
  scripts/ 子目录约定收进模板
- 媒体工具段(seedream/seedance 红线)抽成 _MEDIA_TOOLS_BLOCK,仅 ArkConfig.load()
  非 None(有 ARK_API_KEY)时追加;ark_cfg 提前 load 一次复用给 tool 注册
- 路径 echo 全形式段 8→4 行

实测 media_enabled ON/OFF 差 891 字节(=媒体段),命名约定段拼接正常;
test_system_prompt_paths 仍过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-11 08:10:27 +08:00
parent e45705c672
commit d9b48bdb96
3 changed files with 35 additions and 41 deletions

View File

@ -23,6 +23,7 @@
### 2026-06-10 ### 2026-06-10
- **system prompt 精简(瘦身 ~40 行 + 媒体段按需注入)**:`general_v1.md` + `_build_system_prompt` 去冗余:① 「宪法」文件命名约定从 ~25 行压到 ~6 行(只留格式定义 + 注入值 + 一行 current/重定调,操作细节本就由 proposal/ppt skill 各自讲,引用仍成立);② run_python「先 write script 再 script_path」指引去重(原模板 + agent_builder 两处 → 合并进模板 1 处,顺带把 `scripts/` 子目录约定收进去);③ 媒体工具段(seedream/seedance 红线)从常驻模板抽成 `_MEDIA_TOOLS_BLOCK`,仅 `ArkConfig.load() is not None`(有 ARK_API_KEY)时由 agent_builder 追加——无 key 用户不再背 7 行永远报错工具的说明,且 ark_cfg 提前 load 一次复用给下方 tool 注册;④ 「路径 echo 全形式」段 8 行压到 4 行。通用任务每轮 system prompt 净瘦 ~40-50 行,领域 task 加载 skill 后信息不丢。`test_system_prompt_paths` 仍过。
- **上下文压缩加压力门槛**:压缩只在总 chars 超阈值(`caps.reliable_context×0.5×2.5 char/token`,flash ≈33 万)时才做,未超则原样发——护 DeepSeek 前缀缓存(短任务字节逐轮一致、全程命中)+ 不白丢旧细节。`prepare_messages_with_stats(compact_threshold_chars=)`,`compaction_skipped` 进事件;默认 0=向后兼容永远压。实测高轮 task 缓存命中已 92-94%,故只补门槛不改滑动边界。+2 测试。 - **上下文压缩加压力门槛**:压缩只在总 chars 超阈值(`caps.reliable_context×0.5×2.5 char/token`,flash ≈33 万)时才做,未超则原样发——护 DeepSeek 前缀缓存(短任务字节逐轮一致、全程命中)+ 不白丢旧细节。`prepare_messages_with_stats(compact_threshold_chars=)`,`compaction_skipped` 进事件;默认 0=向后兼容永远压。实测高轮 task 缓存命中已 92-94%,故只补门槛不改滑动边界。+2 测试。
- **单轮停机判据从「步数」解耦为「是否在推进」**:`max_iterations` 从「轮预算」降级为纯安全 backstop(flash 50→120 / pro 100→150),真正掐空转靠两道进展信号——`_RepeatGuard` 逐指纹「无产出重复」累计(SOFT2 注提示 / HARD4 拦截)+ run 级全局 `_stall`(整步所有 tool 无净产出 +1、任一净产出清零,连续 8 步主动停)。撞 backstop / 熔断都 emit「回复『继续』可续跑」提示,不静默停。(诊断:task `b27466a0` 所谓「中途断」实为撞旧 50 步上限干净停下。) - **单轮停机判据从「步数」解耦为「是否在推进」**:`max_iterations` 从「轮预算」降级为纯安全 backstop(flash 50→120 / pro 100→150),真正掐空转靠两道进展信号——`_RepeatGuard` 逐指纹「无产出重复」累计(SOFT2 注提示 / HARD4 拦截)+ run 级全局 `_stall`(整步所有 tool 无净产出 +1、任一净产出清零,连续 8 步主动停)。撞 backstop / 熔断都 emit「回复『继续』可续跑」提示,不静默停。(诊断:task `b27466a0` 所谓「中途断」实为撞旧 50 步上限干净停下。)
- **`systemctl restart` 优雅 drain in-flight run**:restart 不再硬杀 BG run 致 reaper 误标 error。纯进程内零 DB 改动:lifespan 加 `draining` + `inflight` 登记,先拒新 run(503+Retry-After)再 `asyncio.wait(drain_timeout)` 收尾,超时转协作式 cancel。部署强耦合:unit `TimeoutStopSec` 提到 90(必须 > drain+grace),前端发送包退避重试。 - **`systemctl restart` 优雅 drain in-flight run**:restart 不再硬杀 BG run 致 reaper 误标 error。纯进程内零 DB 改动:lifespan 加 `draining` + `inflight` 登记,先拒新 run(503+Retry-After)再 `asyncio.wait(drain_timeout)` 收尾,超时转协作式 cancel。部署强耦合:unit `TimeoutStopSec` 提到 90(必须 > drain+grace),前端发送包退避重试。

View File

@ -58,6 +58,20 @@ from core.ark_client import ArkConfig
from core.bocha_client import BochaConfig from core.bocha_client import BochaConfig
# 媒体工具(seedream / seedance)指引:仅当本 run 真的挂了媒体工具(ARK_API_KEY 存在,
# ArkConfig.load() 非 None)才追加进 system prompt —— 没 key 的用户不会看到永远报错的工具,
# 也不该背这段红线。文案与 base 模板里其余工具表平级,放在 _build_system_prompt 里按需拼。
_MEDIA_TOOLS_BLOCK = """\
## 媒体生成工具(seedream 图 / seedance 视频)
- `seedream` 豆包图像生成产物自动落 `<task_dir>/figures/`每次 **¥0.22**(联网 `search=true` ¥0.05)
- **调用前必须先 `load_skill('imagegen')`** skill 里有何时该用 / 该不该用 mermaid 替代 / 用户描述模糊度诊断 / 一次性追问范式 / prompt 装配 / 失败解药全套引导**不要拿用户原话直接当 prompt tool** 容易烧 ¥0.22 在错的方向上
- 兜底硬约束(即使没 load skill 也守):用户没主动要图就别装饰性生成;同一目的不满意**不要连发**,先口头校准 prompt 再调
- `seedance` 豆包视频生成(Seedance 2.0 Fast)异步任务,** 30-90s 出片**;产物自动落 `<task_dir>/videos/`每次 **¥1.86 **(480p 4s)~ **¥12+**(720p 15s),比图贵 10 倍以上触发词:视频 / 动画 / 动起来 / 做个 video / 镜头 / 短片 / 演示视频 / 动效
- **调用前必须先 `load_skill('videogen')`** skill 里有6 维诊断(含运动维必填)/ seedream/mermaid 反向选型 / prompt 装配 / 参数取舍(时长/分辨率/比例直接决定钱)/ 失败解药全套引导视频比图贵 10 倍且 90s 等待,绝对不要拿用户原话当 prompt 直接调
- 兜底硬约束:用户没主动要视频就别装饰性生成(比生图更严重的红线);同一目的不满意**绝不连发**(1 次错 = ¥4+60s,连发 2 = ¥8+2min);phase 1 仅文生视频,**不支持** image-to-video / video-to-video"""
def load_config() -> dict: def load_config() -> dict:
return yaml.safe_load((ROOT / "config" / "agent.yaml").read_text(encoding="utf-8")) or {} return yaml.safe_load((ROOT / "config" / "agent.yaml").read_text(encoding="utf-8")) or {}
@ -199,6 +213,7 @@ def _build_system_prompt(
task_id: UUID, task_id: UUID,
task_name: str, task_name: str,
task_skill: str = "", task_skill: str = "",
media_enabled: bool = False,
) -> str: ) -> str:
"""拼 system prompt: 模板 + skill 列表 + memory + 工作目录段 + task 上下文 + 命名约定。 """拼 system prompt: 模板 + skill 列表 + memory + 工作目录段 + task 上下文 + 命名约定。
@ -213,6 +228,8 @@ def _build_system_prompt(
if skills.skills: if skills.skills:
prompt += f"\n\n## 可用 skill (用 load_skill 加载完整指引)\n{skills.discovery_block()}" prompt += f"\n\n## 可用 skill (用 load_skill 加载完整指引)\n{skills.discovery_block()}"
prompt += memory_block(workspace_dir, user_id) prompt += memory_block(workspace_dir, user_id)
if media_enabled:
prompt += "\n\n" + _MEDIA_TOOLS_BLOCK
# docker backend 下 shell/run_python/fs 工具全在容器里跑,容器把 # docker backend 下 shell/run_python/fs 工具全在容器里跑,容器把
# `<workspace>/users/<uid>` bind 到 `/workspace`、`--workdir /workspace/<wd>` # `<workspace>/users/<uid>` bind 到 `/workspace`、`--workdir /workspace/<wd>`
# (executor_docker.py:99-100)。此时 prompt 必须给**容器路径**,否则 LLM # (executor_docker.py:99-100)。此时 prompt 必须给**容器路径**,否则 LLM
@ -260,33 +277,16 @@ def _build_system_prompt(
f"普通产物(sections / slides / 终稿 .docx/.pptx)按 SKILL 文档落路径;" f"普通产物(sections / slides / 终稿 .docx/.pptx)按 SKILL 文档落路径;"
f"「宪法」性文件(spec 等)按下面《task 级「宪法」文件命名约定》拼路径。\n" f"「宪法」性文件(spec 等)按下面《task 级「宪法」文件命名约定》拼路径。\n"
f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。\n" f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。\n"
f"\n**run_python 过程脚本**:非平凡的 Python(>~15 行 / 要迭代调试 / 生成产物)"
f"先用写文件工具落到 `<task_dir>/scripts/`(如 `scripts/analyze.py`,父目录自动建),"
f"再用 `run_python(script_path=\"scripts/analyze.py\")` 执行 —— 源码留在文件里、可重读可改可重跑,"
f"不挤占对话上下文。`scripts/` 只放过程脚本,**交付产物(.docx/.pptx/spec/figures 等)仍落 task_dir 根或 SKILL 指定路径**。"
f"真·一次性短代码(算个数 / 探查一行)才用 `run_python(code=...)` 内联,临时执行不留痕。\n"
f"\n## task 级「宪法」文件命名约定(跨 skill 通用)\n" f"\n## task 级「宪法」文件命名约定(跨 skill 通用)\n"
f"任何 skill 产物中,跟 task 1:1 强绑定、阶段二/后续步骤会**反复 read**" f"跟 task 1:1 绑定、后续步骤会**反复 read** 的「宪法」性文件(如 proposal/ppt 的 "
f"的「宪法」性文件(如 proposal/ppt 的 spec、outline 等),**统一按下面格式命名**," f"spec、outline),统一落 task_dir 根、按此格式命名:\n\n"
f"落在 task_dir 根下:\n\n"
f" <YYYY-MM-DD>-<task_short_id>-<task_name>.<base>.md\n\n" f" <YYYY-MM-DD>-<task_short_id>-<task_name>.<base>.md\n\n"
f"其中 `<YYYY-MM-DD>` = 本会话 today=`{today}`;" f"用上面注入的值:`<YYYY-MM-DD>`=today=`{today}`、`<task_short_id>`=`{short_id}`"
f"`<task_short_id>` = `{short_id}`(永不变,主锚);" f"(永不变主锚)、`<task_name>`=`{tname}`(原样用 含 CJK/空格);`<base>` 由 skill "
f"`<task_name>` = `{tname}`(可变,人类可读说明,原样用 含 CJK / 空格);" f"定义(如 `spec`)。取 current:按 short_id glob `{wd_path}/*-{short_id}-*.<base>.md`"
f"`<base>` 由 skill 定义(如 proposal/ppt 的 `spec`)。\n\n" f" → 文件名字典序取最大者(= 最新日期,改过 task_name 旧文件仍能定位);重定调时以 "
f"**取 current 版本规则**:read 时 **按 task_short_id 锚定** glob " f"today 为前缀写新版、**旧版留作历史快照不要覆盖**(同日多版加 `-v2`/`-v3`)。"
f"`{wd_path}/*-{short_id}-*.<base>.md` → 按文件名字典序排 → 取最大者" f"取用 / 重定调的具体时机见对应 skill。"
f"(= 最新日期)。这样即使用户改了 task_name,旧文件仍能定位(`<task_name>` "
f"那段视为「建时快照」,不强求同步)。这是「current 指针」的纯文件名实现,"
f"agent 自己拼即可。\n\n"
f"**重定调场景**:用户阶段一已确认过的「宪法」文件,后续要推翻重写时,"
f"以 today=`{today}` 为前缀写一份新的,**旧版自然保留为历史快照**(不要 edit "
f"覆盖旧文件)。同日多次重定调可在文件名末尾加 `-v2` / `-v3` 等递增后缀。\n\n"
f"**隔离逻辑**:同 working_dir 多 task → 由 `<task_short_id>` 严格隔离"
f"(8 位 hex,撞概率近 0);同 task 多版本 → 由 `<YYYY-MM-DD>` 隔离。两层隔离"
f"都靠文件名,**无目录嵌套、无 DB 字段、无 cascade rename**。其余产物"
f"(`sections/` / `figures/` / `slides/` / 终稿 .docx/.pptx 等)按 SKILL "
f"文档保留扁平共享,LLM 自行通过 task_short_id / 命名前缀判断归属。"
) )
return prompt return prompt
@ -370,6 +370,10 @@ def build_agent(
skills = SkillRegistry(ROOT / cfg.get("skills_dir", "skills")) skills = SkillRegistry(ROOT / cfg.get("skills_dir", "skills"))
# 媒体配置提前 load 一次:既决定 system prompt 要不要追加媒体段(media_enabled),
# 也复用给下方 seedream/seedance 注册(避免重复读 doubao.yaml)。无 ARK_API_KEY → None。
ark_cfg = ArkConfig.load()
now_iso = datetime.now().isoformat(timespec="seconds") now_iso = datetime.now().isoformat(timespec="seconds")
# meta["working_dir"] 是 db 形态(相对 ROOT 或绝对);Session.append → ensure_local_task_row # meta["working_dir"] 是 db 形态(相对 ROOT 或绝对);Session.append → ensure_local_task_row
# 把它直接落 PG tasks.working_dir,所以这里就转好。文件系统操作仍用 working_dir_path(absolute)。 # 把它直接落 PG tasks.working_dir,所以这里就转好。文件系统操作仍用 working_dir_path(absolute)。
@ -402,6 +406,7 @@ def build_agent(
system_prompt = _build_system_prompt( system_prompt = _build_system_prompt(
cfg, skills, workspace_dir, tool_base, working_dir_path, uid, cfg, skills, workspace_dir, tool_base, working_dir_path, uid,
task_id, task_state.name, task_state.skill, task_id, task_state.name, task_state.skill,
media_enabled=ark_cfg is not None,
) )
meta = { meta = {
@ -503,7 +508,7 @@ def build_agent(
# image_variant 由 caller 传(web 入口随消息 POST 带);空 → 取 yaml 第一个 variant # image_variant 由 caller 传(web 入口随消息 POST 带);空 → 取 yaml 第一个 variant
# (fallback,沿用原行为)。本次 run 装的 SeedreamTool 锁定该 variant,本 run 内的 # (fallback,沿用原行为)。本次 run 装的 SeedreamTool 锁定该 variant,本 run 内的
# 多次 tool call 全用同一个;下一条消息可以重选。 # 多次 tool call 全用同一个;下一条消息可以重选。
ark_cfg = ArkConfig.load() # ark_cfg 已在函数上半部 load 过(复用,顺带决定 system prompt 的 media 段)。
if ark_cfg is not None: if ark_cfg is not None:
image_cfg = (ark_cfg.raw.get("image") or {}) image_cfg = (ark_cfg.raw.get("image") or {})
chosen_key, chosen_cfg = "", None chosen_key, chosen_cfg = "", None

View File

@ -4,7 +4,7 @@
- `read` / `write` / `edit` —— 文件操作 - `read` / `write` / `edit` —— 文件操作
- `glob` / `grep` —— 文件搜索 - `glob` / `grep` —— 文件搜索
- `shell` —— 执行命令(默认 60s 超时) - `shell` —— 执行命令(默认 60s 超时)
- `run_python` —— 在子进程里跑 Python (数据处理、生成 .pptx/.docx、画图等)。非短小一次性代码时,先用 `write``.py` 文件写到 task_dir,再用 `run_python(script_path="...")` 执行;避免大段源码进入对话历史 - `run_python` —— 在子进程里跑 Python (数据处理、生成 .pptx/.docx、画图等)。非短小一次性代码时,先用 `write``.py` 落到 `<task_dir>/scripts/`(如 `scripts/analyze.py`),再 `run_python(script_path="scripts/analyze.py")` 执行 —— 源码留文件里可重读可改可重跑,不挤占对话历史;`scripts/` 只放过程脚本,交付产物仍落 task_dir 根或 SKILL 指定路径。真·一次性短代码(算个数/探查一行)才用 `run_python(code=...)` 内联
- `load_skill` —— 加载某个 skill 的完整指引 - `load_skill` —— 加载某个 skill 的完整指引
- `task_progress` —— 给 Web 前端发布/更新用户可见的进度步骤列表。只在多步骤任务使用;开始时设 3-7 个关键步骤,每完成或进入一个关键步骤时更新一次。 - `task_progress` —— 给 Web 前端发布/更新用户可见的进度步骤列表。只在多步骤任务使用;开始时设 3-7 个关键步骤,每完成或进入一个关键步骤时更新一次。
@ -15,14 +15,6 @@
- 任务全部做完时,把最后一步标成 `completed`(让用户在顶部进度面板看到"全绿"收尾),**不要用 `clear`**;`clear` 只在计划被推翻、不再相关时才用。 - 任务全部做完时,把最后一步标成 `completed`(让用户在顶部进度面板看到"全绿"收尾),**不要用 `clear`**;`clear` 只在计划被推翻、不再相关时才用。
- 简单问答、单次文件读取、很小的改动不需要调用 `task_progress` - 简单问答、单次文件读取、很小的改动不需要调用 `task_progress`
## 媒体生成工具(按需可用,未配置 ARK_API_KEY 时该工具不会出现)
- `seedream` —— 豆包图像生成。产物自动落 `<task_dir>/figures/`。每次 **¥0.22**(联网 `search=true` 加 ¥0.05)。
- **调用前必须先 `load_skill('imagegen')`** —— skill 里有「何时该用 / 该不该用 mermaid 替代 / 用户描述模糊度诊断 / 一次性追问范式 / prompt 装配 / 失败解药」全套引导。**不要拿用户原话直接当 prompt 调 tool** —— 容易烧 ¥0.22 在错的方向上。
- 兜底硬约束(即使没 load skill 也守):用户没主动要图就别装饰性生成;同一目的不满意**不要连发**,先口头校准 prompt 再调。
- `seedance` —— 豆包视频生成(Seedance 2.0 Fast)。异步任务,**等 30-90s 出片**;产物自动落 `<task_dir>/videos/`。每次 **¥1.86 起**(480p 4s)~ **¥12+**(720p 15s),比图贵 10 倍以上。触发词:视频 / 动画 / 动起来 / 做个 video / 镜头 / 短片 / 演示视频 / 动效。
- **调用前必须先 `load_skill('videogen')`** —— skill 里有「6 维诊断(含运动维必填)/ seedream/mermaid 反向选型 / prompt 装配 / 参数取舍(时长/分辨率/比例直接决定钱)/ 失败解药」全套引导。视频比图贵 10 倍且 90s 等待,绝对不要拿用户原话当 prompt 直接调。
- 兜底硬约束:用户没主动要视频就别装饰性生成(比生图更严重的红线);同一目的不满意**绝不连发**(1 次错 = ¥4+60s,连发 2 次 = ¥8+2min);phase 1 仅文生视频,**不支持** image-to-video / video-to-video。
## Skill 机制 ## Skill 机制
你启动时只看到下方 skill 的"名字 + 描述"。Skill 是**可选辅助** —— 任务明确落在 你启动时只看到下方 skill 的"名字 + 描述"。Skill 是**可选辅助** —— 任务明确落在
某个 skill 领域(用户要做 PPT、写申报书等)时,先 `load_skill(name)` 拿完整指引 某个 skill 领域(用户要做 PPT、写申报书等)时,先 `load_skill(name)` 拿完整指引
@ -43,13 +35,9 @@
- 少来回:多个**互相独立、不依赖中间结果**的操作(建多页产物、批量改文件、生成整份 deck/文档)合到一个脚本或一轮(并发多 tool call)里做,别一步一个 tool call —— 每轮来回都重发整段上下文,轮数是 token 体量的线性乘数;但**下一步输入要看上一步结果**时(探索性检索、按报错改、需用户确认方向)就老实分步,别硬批 - 少来回:多个**互相独立、不依赖中间结果**的操作(建多页产物、批量改文件、生成整份 deck/文档)合到一个脚本或一轮(并发多 tool call)里做,别一步一个 tool call —— 每轮来回都重发整段上下文,轮数是 token 体量的线性乘数;但**下一步输入要看上一步结果**时(探索性检索、按报错改、需用户确认方向)就老实分步,别硬批
## 路径 ## 路径
默认工作目录在系统消息末尾,所有相对路径基于该目录 默认工作目录见系统消息末尾,相对路径都基于它
**对外 echo 产物文件路径(回复用户、汇报产物)时**:用 user_root 相对的**全形式** `<wd_name>/<rel>` —— `<wd_name>` 就是上方 task_dir 字段的最后一段(如 task_dir = `D:\...\users\<uuid>\生图测试``<wd_name>` = `生图测试`)。例:`生图测试/videos/xxx.mp4`、`生图测试/figures/cover.png`、`基金申报/sections/01-绪论.md`、`公司汇报/slides/deck.pptx`。**不要简写**为 `videos/xxx.mp4` / `figures/cover.png` / `slides/deck.pptx` 这种只在 task 内成立的裸形式。 **对外 echo 产物路径(回复 / 汇报用)一律用全形式 `<wd_name>/<rel>`** —— `<wd_name>` = 上方 task_dir 末段(如末段是 `生图测试``生图测试/figures/cover.png`、`基金申报/sections/01-绪论.md`)。**别简写**成 `figures/cover.png` 这种 task 内裸形式:Web UI 靠 `<wd_name>/` 前缀挂可点 chip(预览 / 下载),简写会失效。媒体 tool 的 `saved:` 行已是规范全形式,原样照抄即可。
媒体 tool(`seedream` / `seedance`)输出的 `saved:` 那行**已经是规范全形式**,原样照抄就行(免去自己拼前缀);其他场景(ppt / proposal / coding 等 `run_python` / `write` / `shell` 写完文件后)自己按 `<wd_name>/<rel>` 拼。
**为什么硬性约束**:Web UI 按 `<wd_name>/...` 前缀识别产物路径挂可点 chip(预览 / 下载);简写形式 chip 失效,用户没法直接点开。跨所有产物 skill 统一生效。
## 平台 ## 平台
当前是 Windows + cmd.exe。**避免用 unix-only flag**: 当前是 Windows + cmd.exe。**避免用 unix-only flag**: