Skip to content

Commit 62a025c

Browse files
fix(selling): distinguish new vs existing VPC in architecture planning
architecture_planning 在 intent 未声明 VPC 已存在时仍预设已有 VPC, 只产出「已有 VPC 下新建 VSwitch」单候选,候选集遗漏用户的 VPC 创建需求。 - iac-aliyun-architecture skill 明确 use_existing/reference 只能来自 intent 显式声明,未声明的资源(含 resource_intents 缺失)默认按 create 处理 - 承载关系中被依赖资源生命周期未确认时,要求同时给出「新建 VPC + 新建 VSwitch」和「复用已有 VPC + 新建 VSwitch」两个候选;仅在 intent 显式 声明或 network_constraints 给出已有资源标识后才收敛为单候选 - 收紧「简单明确需求给单候选」口径为资源清单与生命周期均已确定 - 补充两个 eval 用例与契约测试覆盖未确认/已确认两种语义 evaluate_candidates 为引擎驱动的 parallel_sub_pipeline,只消费 architecture.candidates,不产生候选,无需改动。
1 parent d470b40 commit 62a025c

4 files changed

Lines changed: 126 additions & 1 deletion

File tree

‎src/iac_code/pipeline/selling/prompts/architecture_planning.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
根据用户意图生成差异化的候选架构方案。方案数量取决于需求复杂度:
77
- 简单明确需求(如"创建一个 VPC"):只给 1 个方案
88
- 有设计空间的需求(如"部署一个 Web 应用"):给出 2-3 个有实质差异的方案
9+
- 依赖资源是新建还是复用已有尚未确认时(如意图只有 VPC、VSwitch 且没有声明 VPC 已存在):至少给出 2 个方案,分别覆盖「新建被依赖资源」和「复用已有被依赖资源」
910

1011
## 用户意图(上一步结论)
1112
```json
@@ -26,4 +27,5 @@
2627
- 你可以按需自主使用 `read_memory` 补充规划上下文:在生成方案前,如用户意图涉及已有资源、默认地域、已有 VPC/Zone、网段约束、成本偏好、高可用偏好、架构偏好、命名规范或历史项目约束,先调用 `read_memory({})` 查看索引,再读取相关 name。
2728
- 记忆只用于补充方案设计背景;若记忆与当前用户意图冲突,以当前用户意图为准。
2829
- 直接根据已知意图设计架构方案。
30+
- `resource_intents` 里没有显式标成 `use_existing` / `reference` 的资源默认按新建处理;意图没有声明某资源已存在时,不要预设它是已有资源。
2931
- 如果意图信息不足以设计架构,可在 rollback_request 中请求回退到 intent_parsing。

‎src/iac_code/pipeline/selling/skills/iac-aliyun-architecture/SKILL.md‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ conclusion_schema:
6969

7070
判断标准:如果你需要添加用户完全没提到的产品来"制造"差异,那就不该有多个方案。
7171

72+
「简单明确」指的是**资源清单和生命周期都已确定**。如果意图里的资源本身很少,但某个依赖资源是新建还是复用尚未确认,那它不属于简单明确需求,必须按下面「依赖资源生命周期未确认时的候选覆盖」给出多个候选,不能因为资源数量少就收敛成单候选。
73+
7274
## 差异化维度
7375

7476
当需求确实存在设计取舍时,根据场景从以下维度中选择最相关的来构建差异方案:
@@ -105,6 +107,27 @@ conclusion_schema:
105107

106108
示例:intent 表示“已有 VPC 中创建安全组”时,candidate 应包含 `resource_intents: [{"product": "VPC", "action": "use_existing"}, {"product": "SecurityGroup", "action": "create"}]`。不得生成 VSwitch,也不得设计成“创建 VPC + VSwitch + SecurityGroup”。
107109

110+
### 未声明的资源默认是新建
111+
112+
`use_existing` / `reference` 只能来自 intent 的显式声明,不能由你推断:
113+
114+
- intent 的 `resource_intents` 没有把某个 product 标成 `use_existing` 或 `reference` 时,该资源默认按 `action=create` 处理。
115+
- intent 完全没有 `resource_intents` 字段时,`core_requirements` 里的每个 product 都默认按 `action=create` 处理。
116+
- 不得因为某个资源“通常已经存在”“一般由用户提前准备好”就预设已有资源。没有显式声明就当作本次新建。
117+
118+
反例:intent 只解析出 `VPC`、`VSwitch` 且没有任何 `use_existing` 声明时,把 VPC 预设成已有 VPC、只产出「已有 VPC 下新建 VSwitch」单候选是错误的——这会让候选集完全遗漏用户的 VPC 创建需求。
119+
120+
### 依赖资源生命周期未确认时的候选覆盖
121+
122+
当意图里存在承载关系(如 VSwitch 依赖 VPC、ECS 依赖 VSwitch、安全组依赖 VPC),而**被依赖资源到底是新建还是复用已有还没有确认**时,必须给出至少两个候选,分别覆盖两种语义:
123+
124+
- 候选 A:被依赖资源 `action=create`,与目标资源一起新建(如「新建 VPC + 新建 VSwitch」)。
125+
- 候选 B:被依赖资源 `action=use_existing`,只新建目标资源(如「复用已有 VPC + 新建 VSwitch」)。
126+
127+
「已确认」只有两种来源:intent 的 `resource_intents` 已显式给出该资源的 `use_existing` / `reference` / `create`,或 `non_functional.network_constraints` 已给出具体的已有资源标识(如 VPC ID、VSwitch ID、既有网段)。任一来源确认后,按确认的语义收敛为单候选,不要再为了凑数保留另一种。
128+
129+
每个候选的 `resource_intents` 必须与该候选自身的语义一致:候选 A 里的 VPC 是 `create`,候选 B 里的 VPC 是 `use_existing`。不要让两个候选共用同一份继承自 intent 的 `resource_intents`。
130+
108131
## 输出
109132
调用 `complete_step` 提交结论。字段定义见 tool schema。
110133

@@ -114,7 +137,7 @@ conclusion_schema:
114137
- 名称为方案名的英文 kebab-case 简写
115138
- 示例:`templates/1-simple-nginx.yml`、`templates/2-high-availability-slb.yml`
116139

117-
当只有 1 个方案时,`candidates` 只有 1 个元素。
140+
当只有 1 个方案时,`candidates` 只有 1 个元素。只有资源清单和生命周期都已确定时才允许收敛到 1 个方案。
118141

119142
## 约束
120143

‎src/iac_code/pipeline/selling/skills/iac-aliyun-architecture/evals.json‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,57 @@
172172
{"name": "within_budget", "check": "所有方案的monthly_cost_range上限不超过500 CNY"},
173173
{"name": "no_overengineering", "check": "不包含SLB集群、多可用区等超出预算的高可用配置"}
174174
]
175+
},
176+
{
177+
"id": 9,
178+
"name": "vpc-vswitch-lifecycle-unconfirmed",
179+
"prompt": "创建一个VPC和一个VSwitch",
180+
"intent_context": {
181+
"is_infra_intent": true,
182+
"confidence": "high",
183+
"cloud_platform": "aliyun",
184+
"business_type": "网络基础设施",
185+
"core_requirements": ["VPC", "VSwitch"],
186+
"resource_intents": [
187+
{"product": "VPC", "action": "create", "source": "user"},
188+
{"product": "VSwitch", "action": "create", "source": "user"}
189+
],
190+
"non_functional": {"high_availability": false, "security_level": "basic"},
191+
"scale_hint": null,
192+
"budget_constraint": null,
193+
"additional_notes": "用户未声明 VPC 已存在"
194+
},
195+
"expected_behavior": "意图未声明 VPC 已存在,必须覆盖新建 VPC 的需求;不得只产出「已有 VPC 下新建 VSwitch」单候选",
196+
"assertions": [
197+
{"name": "covers_vpc_creation", "check": "至少一个方案的resource_intents中VPC的action为create"},
198+
{"name": "no_presumed_existing_vpc", "check": "不存在只有「已有VPC下新建VSwitch」这一个候选的情况;候选数量至少为2"},
199+
{"name": "per_candidate_lifecycle", "check": "每个方案的resource_intents与该方案自身语义一致,不共用同一份继承值"}
200+
]
201+
},
202+
{
203+
"id": 10,
204+
"name": "vswitch-in-confirmed-existing-vpc",
205+
"prompt": "在已有的 vpc-bp1abc 下创建一个VSwitch",
206+
"intent_context": {
207+
"is_infra_intent": true,
208+
"confidence": "high",
209+
"cloud_platform": "aliyun",
210+
"business_type": "网络基础设施",
211+
"core_requirements": ["VPC", "VSwitch"],
212+
"resource_intents": [
213+
{"product": "VPC", "action": "use_existing", "role": "network_container", "source": "user"},
214+
{"product": "VSwitch", "action": "create", "source": "user"}
215+
],
216+
"non_functional": {"network_constraints": {"vpc_id": "vpc-bp1abc"}},
217+
"scale_hint": null,
218+
"budget_constraint": null,
219+
"additional_notes": "用户已明确给出已有 VPC ID"
220+
},
221+
"expected_behavior": "已有 VPC 上下文已确认,收敛为单候选「已有 VPC 下新建 VSwitch」",
222+
"assertions": [
223+
{"name": "single_candidate", "check": "candidates数组长度为1"},
224+
{"name": "vpc_use_existing", "check": "方案的resource_intents中VPC的action为use_existing,不生成新建VPC"}
225+
]
175226
}
176227
]
177228
}

‎tests/pipeline/selling/skills/test_iac_aliyun_architecture_skill.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
from pathlib import Path
45

56
SKILL_DIR = (
@@ -34,3 +35,51 @@ def test_architecture_prompt_guides_optional_memory_lookup_for_planning_context(
3435
assert "架构偏好" in body
3536
assert "已有 VPC" in body
3637
assert "当前用户意图为准" in body
38+
39+
40+
def test_architecture_treats_undeclared_resources_as_create():
41+
body = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
42+
43+
assert "未声明的资源默认是新建" in body
44+
assert "默认按 `action=create` 处理" in body
45+
assert "没有显式声明就当作本次新建" in body
46+
assert "不得因为某个资源“通常已经存在”" in body
47+
48+
49+
def test_architecture_requires_both_candidates_when_dependency_lifecycle_unconfirmed():
50+
body = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
51+
52+
assert "依赖资源生命周期未确认时的候选覆盖" in body
53+
assert "必须给出至少两个候选" in body
54+
assert "新建 VPC + 新建 VSwitch" in body
55+
assert "复用已有 VPC + 新建 VSwitch" in body
56+
assert "non_functional.network_constraints" in body
57+
assert "按确认的语义收敛为单候选" in body
58+
assert "不要让两个候选共用同一份继承自 intent 的 `resource_intents`" in body
59+
60+
61+
def test_architecture_simple_requirement_shortcut_does_not_bypass_lifecycle_coverage():
62+
body = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
63+
prompt = PROMPT_FILE.read_text(encoding="utf-8")
64+
65+
assert "资源清单和生命周期都已确定" in body
66+
assert "不能因为资源数量少就收敛成单候选" in body
67+
assert "至少给出 2 个方案" in prompt
68+
assert "不要预设它是已有资源" in prompt
69+
70+
71+
def test_architecture_evals_cover_unconfirmed_and_confirmed_vpc_lifecycle():
72+
data = json.loads((SKILL_DIR / "evals.json").read_text(encoding="utf-8"))
73+
evals_by_name = {ev["name"]: ev for ev in data["evals"]}
74+
75+
unconfirmed = evals_by_name["vpc-vswitch-lifecycle-unconfirmed"]
76+
assert unconfirmed["intent_context"]["core_requirements"] == ["VPC", "VSwitch"]
77+
assert all(item["action"] == "create" for item in unconfirmed["intent_context"]["resource_intents"])
78+
assertion_names = {assertion["name"] for assertion in unconfirmed["assertions"]}
79+
assert "covers_vpc_creation" in assertion_names
80+
assert "no_presumed_existing_vpc" in assertion_names
81+
82+
confirmed = evals_by_name["vswitch-in-confirmed-existing-vpc"]
83+
vpc_intent = next(item for item in confirmed["intent_context"]["resource_intents"] if item["product"] == "VPC")
84+
assert vpc_intent["action"] == "use_existing"
85+
assert "single_candidate" in {assertion["name"] for assertion in confirmed["assertions"]}

0 commit comments

Comments
 (0)