削减策划agent的工作流负担 #373

Merged
lhk229 merged 10 commits from opt/design_simplify into master 2026-09-15 18:49:00 +08:00
9 changed files with 137 additions and 774 deletions
File diff suppressed because one or more lines are too long
@@ -74,7 +74,7 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
三个接口:**对上**承顶层系统范围表并跑循环覆盖检查;**对内**地图↔职责↔依赖
三方一致、主数据归属唯一;**对下**目录映射 + MVP 闭环喂系统文档站。
## 四、怎么写(模板即流程,十二节按序
## 四、怎么写(模板参考结构,建议按此组织
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/architecture.md
### 1. 架构定位与目标
@@ -85,7 +85,7 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
→ 没有变更记录的架构文档,第二轮迭代就会变成黑箱。
### 2. 系统地图
Sxx 编号清单(核心系统 2~12 个)+ 支撑层(存档/UI,不拥有核心规则)。
Sxx 编号清单(核心系统通常 1-5 个,有明确要求可超出 5 个)+ 支撑层(存档/UI,不拥有核心规则)。
P0 段五列表:
| 系统 | 目的 | 输入 | 输出 | P0 原因 |
→ 每行 P0 原因必须答"删了它,__ 塌";答不出的降级或合并。
@@ -69,7 +69,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
记住三个接口:**对内**锚点仲裁一切;**对下**张力变取舍表、定稿变硬约束;
**对上**边界画线防止越层。九节不是清单,是一台咬合的机器。
## 四、怎么写(模板即流程,九节按序
## 四、怎么写(模板参考结构,建议按此组织
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/concept-design.md
### 1. 一句话概念
@@ -2,7 +2,7 @@
---
name: game-gdd-system-doc
description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二节同构骨架
description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二类常见内容
红线与分析文档格式。每类系统的专属写法与模板在 modules/system-types/ 下对应目录的 SKILL.md
与对应模块的模板.md 里,按需取用。
---
@@ -26,9 +26,9 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。
2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗
的判定部分),读取对应的 `SKILL.md``模板.md`
3. 该文件夹标注"必读例子"的,先读例子全文做密度锚
3. 该文件夹标注"参考例子"的,先读例子了解写法
## 三、十二节总览:写什么、为什么、怎么咬合
## 三、常见内容总览:写什么、为什么、怎么咬合
系统文档回答四个问题:
**这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→
@@ -52,7 +52,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越
职责边界;**对下**第 7 节交接喂 TDD。
## 四、十二节通用写法
## 四、常见内容的参考写法
(各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md
1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。
@@ -65,7 +65,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
8 反馈:每种关键结果给独立反馈形态;失败必须说明原因和恢复路径。
9 内部循环:动词链;可拆单次/区域/长期三层。
10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。
11 边界与非目标:该类型 skill 的"三不"写全;必含"字段数值归 TDD"一条
11 边界与非目标:参考该类型 skill 的三不”说明边界;建议说明字段数值的交接边界
12 开放问题:结构级才留;手感数值类标"待原型验证"。
## 五、分析文档(全局一份,按层分节)
File diff suppressed because it is too large Load Diff
@@ -80,7 +80,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
(大⇄小⇄最小单位)+ 资源三段全;**对下**系统范围表喂架构的系统地图、
顶层定稿当架构的紧箍咒、验证标准当原型试玩判据。
## 四、怎么写(模板即流程,十六节按序
## 四、怎么写(模板参考结构,建议按此组织
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md
### 1. 顶层定位与规模锚点
@@ -1,12 +1,16 @@
你是游戏策划协作 Agent,与用户持续协作完成游戏设计。像普通策划同事一样交流,使用工作区文件工具读写资料;所有文件路径使用相对路径。根据当前对话、阶段上下文和已有文档决定下一步行动。修改文件后,简要说明修改内容和相对路径。对不确定内容区分用户确认、Agent 建议和待原型验证事项;不要把建议写成用户已确认的决定。
优先完成能够依据已有信息推进的工作,不要为每个设计空白都询问用户。局部、可逆的问题可以先提出合理方案并标为暂定。会影响当前阶段范围、关键规则、下游实现或其他重要方向,且必须由用户决定的问题,应先通过纯文本或问询工具询问,等待用户回答,并据此更新相关产物;不要带着这类未决问题提交阶段审批。
优先完成能够依据已有信息推进的工作,不要为每个设计空白都询问用户。局部、可逆的问题可以先提出合理方案并标为暂定。会影响当前阶段范围、关键规则、下游实现或其他重要方向,且必须由用户决定的问题,应先通过纯文本或问询工具询问,等待用户回答。决定稳定后,再更新受影响的正式产物和必要的过程记录;不要带着这类未决问题提交阶段审批。
分析阶段优先记录当前目标、上层约束、候选方案、取舍、用户已确认或 Agent 暂定的边界,以及必须检查的验收项。除非用户明确要求展开讨论,不要先在回复中逐节起草与正式文档重复的长篇正文;形成结论后直接写入正式产物,再进行一次必要的一致性检查。文件操作前只需说明简短计划、目标文件和主要变化。
正式策划文档在文档头部写明版本标记,例如“版本:v1”。由你自行维护版本号:只有整体修订、阶段性定稿或用户意见造成实质内容变化时才递增;错别字、措辞润色、单个局部修改和小范围补充不单独递增。
阶段审批是每个阶段的最终检查,表示本阶段产物已经完成,无未决内容,交给用户做最终检阅,不承担问询功能。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。
过程文档用于记录关键依据、决定和待办,不要求实时完整,也不应重复正式设计文档。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。
阶段获批后,产物中已经采用的方案作为后续工作的依据,并保留原有决策来源。除非用户主动质疑或出现新的约束冲突,不要反复要求确认历史暂定决定。
用户说“继续”时,继续推进当前阶段最有价值的工作。判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。
@@ -2,7 +2,7 @@
{"type":"function","function":{"name":"get_workflow_status","description":"读取当前策划工作流状态,只返回阶段列表、当前阶段、已批准阶段和待审批阶段;不推进阶段、不提交审批、不修改文件。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
{"type":"function","function":{"name":"list_resources","description":"列出固定资源的逻辑目录、资源 ID、标题和简介。资源是只读的随包文档;不要猜测物理路径。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
{"type":"function","function":{"name":"read_resource","description":"读取一份固定资源文档全文。每次读取一个 resource_id;资源只读。读到未实现占位文档时由你自行判断和处理。","parameters":{"type":"object","properties":{"resource_id":{"type":"string"}},"required":["resource_id"],"additionalProperties":false}}},
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件,优先用于已有文件的小范围修订。先读文件,以唯一且非空的 old_text 精确匹配并替换为 new_text;new_text 为空可删除片段,保留原文并追加可插入。匹配失败不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["path","old_text","new_text"],"additionalProperties":false}}},
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件。使用 old_text/new_text,或使用 edits 一次进行多个独立替换;每个 old_text 必须非空且在原文件中唯一,匹配失败、重复或范围重叠时不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["old_text","new_text"],"additionalProperties":false}}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"delete_path","description":"谨慎使用;永久删除工作区内的文件或目录;目录会连同全部内容递归删除,不备份。先确认目标及删除范围。path 使用相对路径,不能删除工作区根目录,也不能经过链接。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"list_dir","description":"列出工作目录内的文件和目录。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
{"type":"function","function":{"name":"read_file","description":"读取工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
@@ -264,22 +264,45 @@ pub(crate) fn execute_design_file_tool(
}
"patch_file" => {
let relative = required_tool_path(args)?;
let old = args
.get("old_text")
.and_then(Value::as_str)
.ok_or("缺少 old_text")?;
let new = args
.get("new_text")
.and_then(Value::as_str)
.ok_or("缺少 new_text")?;
if old.is_empty() {
return Err("old_text 不能为空".to_string());
}
let edits = if let Some(items) = args.get("edits").and_then(Value::as_array) {
if items.is_empty() {
return Err("edits 不能为空".to_string());
}
items
.iter()
.enumerate()
.map(|(index, item)| {
let old = item
.get("old_text")
.and_then(Value::as_str)
.ok_or_else(|| format!("edits[{index}].old_text 必须是字符串"))?;
let new = item
.get("new_text")
.and_then(Value::as_str)
.ok_or_else(|| format!("edits[{index}].new_text 必须是字符串"))?;
if old.is_empty() {
return Err(format!("edits[{index}].old_text 不能为空"));
}
Ok((old.to_string(), new.to_string()))
})
.collect::<Result<Vec<_>, String>>()?
} else {
let old = args
.get("old_text")
.and_then(Value::as_str)
.ok_or("缺少 old_text")?;
let new = args
.get("new_text")
.and_then(Value::as_str)
.ok_or("缺少 new_text")?;
if old.is_empty() {
return Err("old_text 不能为空".to_string());
}
vec![(old.to_string(), new.to_string())]
};
let (display, path) = resolve_design_workspace_path(root, &relative)?;
if !path.is_file() {
return Ok(Value::String(format!(
"局部修改失败:文件不存在:{display}"
)));
return Err(format!("文件不存在:{display}"));
}
let content =
fs::read_to_string(&path).map_err(|error| format!("读取失败:{error}"))?;
@@ -288,20 +311,52 @@ pub(crate) fn execute_design_file_tool(
} else {
"\n"
};
let old = old.replace("\r\n", "\n").replace('\n', newline);
let new = new.replace("\r\n", "\n").replace('\n', newline);
let count = content.matches(&old).count();
if count != 1 {
return Err(format!(
"原文匹配 {count} 处,需要唯一匹配;请重新读取文件并扩大匹配范围"
));
let normalized = edits
.into_iter()
.map(|(old, new)| {
(
old.replace("\r\n", "\n").replace('\n', newline),
new.replace("\r\n", "\n").replace('\n', newline),
)
})
.collect::<Vec<_>>();
let mut matches = Vec::new();
for (index, (old, new)) in normalized.iter().enumerate() {
let count = content.matches(old).count();
if count == 0 {
return Err(format!("edits[{index}] 原文未找到:{display}"));
}
if count != 1 {
return Err(format!(
"edits[{index}] 原文匹配 {count} 处,必须唯一:{display}"
));
}
let start = content.find(old).expect("count checked");
let end = start + old.len();
if let Some((other_index, _other_start, _other_end)) = matches
.iter()
.find(|(_, other_start, other_end)| start < *other_end && *other_start < end)
{
return Err(format!(
"edits[{index}] 与 edits[{other_index}] 修改范围重叠:{display}"
));
}
matches.push((index, start, end));
let _ = new;
}
crate::write_game_creator_private_file(
&path,
content.replacen(&old, &new, 1).as_bytes(),
"策划工作区文件",
)?;
Ok(Value::String(format!("已局部修改 {display}")))
let mut updated = content.clone();
for (index, start, end) in matches.into_iter().rev() {
let (_, new) = &normalized[index];
updated.replace_range(start..end, new);
}
if updated == content {
return Err(format!("没有产生修改:{display}"));
}
crate::write_game_creator_private_file(&path, updated.as_bytes(), "策划工作区文件")?;
Ok(Value::String(format!(
"已局部修改 {display}{} 处)",
normalized.len()
)))
}
"delete_path" => {
let relative = required_tool_path(args)?;
@@ -645,6 +700,29 @@ mod tests {
)
.expect("patch");
assert!(patched.as_str().unwrap().contains("已局部修改"));
execute_design_file_tool(
root,
"write_file",
&json!({"path":"notes/multi.md","content":"\n\n"}),
)
.expect("write multi");
let multi = execute_design_file_tool(
root,
"patch_file",
&json!({
"path":"notes/multi.md",
"edits":[
{"old_text":"","new_text":""},
{"old_text":"","new_text":""}
]
}),
)
.expect("multi patch");
assert!(multi.as_str().unwrap().contains("2 处"));
assert_eq!(
fs::read_to_string(root.join("design_artifacts/notes/multi.md")).expect("read multi"),
"\n\n"
);
execute_design_file_tool(root, "delete_path", &json!({"path":"notes"}))
.expect("delete dir");
assert!(!root.join("design_artifacts/notes").exists());