Skip to content

fix(prompt): 写死动作不再丢弃用户写的动作细节 - #839

Open
johnnyzhang-eng wants to merge 1 commit into
1024XEngineer:mainfrom
johnnyzhang-eng:fix/838-attack-custom-prompt
Open

fix(prompt): 写死动作不再丢弃用户写的动作细节#839
johnnyzhang-eng wants to merge 1 commit into
1024XEngineer:mainfrom
johnnyzhang-eng:fix/838-attack-custom-prompt

Conversation

@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

Closes #838

问题

前端把用户的一句自由文本拆成两半发过来:action_type 选哪条已调好的管线custom_prompt 说这次具体要什么(quick-start planner 的提示词原文:"让生成复用已有优化管线" + "必须把动作单独写入 actionPrompt")。

后端只读了前一半。后一半进了 CharacterCard.desc —— 而视频路线不读 desc,那条注释自己写着「角色身份由母版图像承载,不读 name / desc」。于是用户写的东西一个字都没进提示词,任务照常成功、照常扣费、帧数时长成色全对。

生产 124/124 条非 custom 任务全中:

walk    73/73   "奔跑" / "原地行走循环,不向前位移" / "制作一套沉稳、略显笨拙且带喜剧感的行走动画"
idle    40/40   "一只小狗低头在饭碗里吃饭" / "疯狂舞动" / "待机动作,人物微微上下晃动"
jump     7/7    "螃蟹原地快速左右摆动身体,挥舞双钳和多条蟹腿"
attack   4/4    "炸开,分裂成非常多只小型蓝色史莱姆" / "施法:角色保持原地站立"

去重后 walk 有 39 种、idle 有 15 种不同文本——这些是用户真写的,不是前端套话。

用户报障的那条(#564)就是这么来的:史莱姆角色 + "炸开分裂成小史莱姆",实际发出去的是 thrust 模板「弓步蓄力、腰侧收招、直线突刺、收回架式」,模型照着画了个金发人形武者。不是模型幻觉,是我们发错了提示词。

改了什么

ActionSpec.detail —— 用户那句细节,叠在模板之上。与 custom_action 分工:那个是整个动作的内容(custom 时没有模板),这个是对模板动作的补充说明。两个都填会炸。

叠加而不是替换。 模板定运动拓扑(走路要腿交替、跳跃要腾空),那正是前端把这句话分类到这个 action_type 的理由;替换掉等于把这次生成降级成 custom。

位置由两条约束夹定:插在模板正文之后、构图约束之前。_framing 那份注释把 SINGLE_SUBJECT_FRAMING 定成收口句,提示词适配器的契约靠它确认公共约束没被绕过 —— 贴在它后面就是悄悄废掉那道检查。

lint(on_template=...) —— 这是本 PR 里唯一一处放宽判据,理由是机制不适用:

判据 机制 叠在模板上时
subthreshold 「幅度低于模型可控分辨率 → 逐帧随机抖」 不适用:要求这句话是画面里唯一的运动。模板已给了大幅步态,"手臂随步伐轻微摆动"是限定词不是弱指令
unanchored_prop 「没交代身体整体怎么动,手里的东西自行漂移」 不适用:判定条件字面是 props and not _hits(text,"body"),而模板正是那句 body
negation 这条通路没有 negative_prompt 照查
hazard_noun 特效名词盖住轮廓、抠图留脏边 照查
shape_prior 断言母版里没有的装备形状 照查
stance_mismatch 非双足角色写人体部位 照查

部署影响(先看这段再决定合不合)

生产那 124 条真实提示词回放了一遍:

放开前(细节句照 custom 的口径查)    47% 被拒(59/124)
按机制分流后                          19% 被拒(24/124)
剩下的 24 条 = negation 19 + hazard_noun 5

那 24 条今天是静默成功的(描述被丢掉、出一段通用动画),合了之后会变成 4xx 并告诉用户怎么改。方向上这是对的(下一步就是付费调用),但它是用户可见的行为变化 —— 举个真实例子:"角色进行走动动作…仅参考人物本身,不参考或添加其他角色" 会被 negation 拒掉,理由是这条通路没有 negative_prompt、否定句里的名词反而会被 latch 进画面。

如果不想一次到位,我可以再提一个改动:细节句命中 negation / hazard 时只丢细节、照旧出模板动画,同时给用户回一条 warning。但那还是"用户的话被丢了",只是不再是静默的。 我倾向直接拒。

验证

  • 变异测试,每条都验过回滚即变红:
    • _detail_clause 恒返回空串(= 回到静默丢弃)→ 6 红
    • 细节贴到收口句之后 → 4 红
    • 细节替换模板而非叠加 → 26 红
    • 拆掉 executor 那处接线 → 4 红
  • CI 原样命令,跑在最后一次编辑之后:ruff check . 通过;export_openapi rc=0 且 openapi.json 无漂移;lint-imports 2 kept / 0 broken;pytest -q --cov=packages 1895 passed, 14 skipped

两处遗留(另开,不在本 PR)

  1. stance 从没被采集过。 windup_character.character_data 96/96 个角色只有 outfits/version/templatesinput_payload 558/558 没传 stance。所以 stance_mismatch 这道门禁在生产中永远不触发 —— 本 PR 让细节句走了这道门,但在前端采集 stance 之前它是不可达的,别拿它当"非人形角色已修好"。
  2. build_attack_prompt 缺省 THRUST 对非人形角色是错的。 它的注释只回避了「断言持械」(提示词库把某个原型角色的装备写死成默认值,导致所有角色被加上斗篷与武器(Refs #171) #195),没回避「断言人形」:弓步、后脚承重、腰侧收招、直线突刺、收回架式,一个史莱姆没有这些部位。要治根得先有 ①。

前端把用户的一句自由文本拆成两半发过来:action_type 选哪条已调好的管线,
custom_prompt 说这次具体要什么。后端只读了前一半 —— 后一半进了
CharacterCard.desc,而视频路线不读 desc,于是用户写的东西一个字都没进提示词。
任务照常成功、照常扣费、帧数时长成色全对,唯一能察觉的方式是看产物。
生产 124/124 条非 custom 任务全中。

ActionSpec 新增 detail,叠在模板之上(模板定运动拓扑,细节说这次要什么),
插在构图约束之前以保住构图约束是收口句这条不变量。

lint 新增 on_template:subthreshold 与 unanchored_prop 两条的机制都要求
"这句话是画面里唯一的运动",叠在模板上时不成立,照查会把"走路时手臂轻微摆动"
判成弱指令。其余各条(否定式/特效名词/装备形状先验/体型不匹配)与叠加无关,照查。

拿 124 条真实提示词回放:放开前 47% 会被拒,按机制分流后 19%,
剩下的全是 negation(19) 与 hazard_noun(5) —— 这两条本来就该拒。

Refs 1024XEngineer#838
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 27, 2026 11:46am

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次审阅覆盖动作细节从入口、ActionSpec、提示词适配器到四条模板构建器的完整链路。细节现在能进入模板,且新增的回归测试覆盖了主要入口;不过提示词输出已发生变化,下面的版本账本需要同步更新,否则新旧产物无法可靠区分。

验证:git diff --check 通过;无法运行聚焦测试,因为当前环境没有安装 pytestuv

return f"{body} {REFERENCE_FIDELITY_LOCK} {SINGLE_SUBJECT_FRAMING}"
clause = detail.strip()
head = f"{body} {clause}" if clause else body
return f"{head} {REFERENCE_FIDELITY_LOCK} {SINGLE_SUBJECT_FRAMING}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Bump the persisted prompt version

with_framing now changes every walk, jump, idle, and attack prompt by inserting the user detail clause, but windup_ai_engine.prompt.PROMPT_VERSION remains v2. CharacterGenerator persists that constant in every GeneratedAction, so outputs created before and after this PR will carry the same version even though their prompts and generation behavior differ; this defeats the documented prompt-version ledger and prevents reliable quality comparisons or migration decisions. Increment PROMPT_VERSION in the same change.

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.47368% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...i_engine/src/windup_ai_engine/strategy/concrete.py 86.66% 2 Missing ⚠️
...ages/ai_engine/src/windup_ai_engine/prompt/lint.py 75.00% 1 Missing ⚠️
...app/src/windup_app/server/orchestrator/executor.py 75.00% 1 Missing ⚠️

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #839   +/-   ##
=======================================
  Coverage   86.47%   86.47%           
=======================================
  Files         297      297           
  Lines       22355    22379   +24     
  Branches     3402     3402           
=======================================
+ Hits        19331    19352   +21     
- Misses       2074     2076    +2     
- Partials      950      951    +1     
Flag Coverage Δ
backend 91.47% <89.47%> (+<0.01%) ⬆️
frontend 80.51% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...s/ai_engine/src/windup_ai_engine/ports/__init__.py 100.00% <ø> (ø)
.../ai_engine/src/windup_ai_engine/prompt/_framing.py 100.00% <100.00%> (ø)
...s/ai_engine/src/windup_ai_engine/prompt/actions.py 100.00% <100.00%> (ø)
...s/ai_engine/src/windup_ai_engine/prompt/adapter.py 100.00% <100.00%> (ø)
...ages/ai_engine/src/windup_ai_engine/prompt/jump.py 100.00% <100.00%> (ø)
...ages/ai_engine/src/windup_ai_engine/prompt/walk.py 100.00% <100.00%> (ø)
...kages/common/src/windup_common/models/character.py 97.05% <100.00%> (+0.08%) ⬆️
...ages/ai_engine/src/windup_ai_engine/prompt/lint.py 98.18% <75.00%> (-1.82%) ⬇️
...app/src/windup_app/server/orchestrator/executor.py 77.82% <75.00%> (+0.29%) ⬆️
...i_engine/src/windup_ai_engine/strategy/concrete.py 89.47% <86.66%> (-0.33%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: 写死动作静默丢弃用户写的动作细节(生产 124/124 条全中)

1 participant