c4 是做什么的?
C4 回答“这个系统是什么,由什么组成,谁调用谁”
在三个缩放级别上。表面是 visualize 引擎:每个层级一个 rataflow 画布,
每个层级一个带描述的部分,containers 框表示所有权。阅读
agents/.agents/skills/visualize/SKILL.md 了解渲染器本身——本技能
只涵盖 C4 添加的部分。
C4 添加了三件 visualize 没有做的事情:
- 层级阶梯。 L0 上下文,然后 L1/L1b 容器,然后 L2/L2b 组件。每个层级的存在是为了激励下一个层级,读者当前所在的层级 可以从框上看到,而不是从图例中看到。
- 事实收集。 C4 必须完整地说明谁调用什么以及为什么。 Scouts 读取代码库并在任何规范存在之前生成一个有证据支持的元素和 关系清单。
- 作为声明的关系。 容器框只包含组件自身的部分 而不包含其他任何内容。这是一个所有权声明,因此它需要 来源,并且必须能够经受读者对照代码检查该框的考验。
不要发明元素、关系或边界。如果 Scouts 不能引用它, 它就不会被绘制。
路由
| 提问 | 使用 |
|---|---|
| 真实代码库中某个组件的 C4 层级 | 本技能 |
| 从已有事实出发的一张关系图 | visualize |
| 聊天回复中的图表 | mermaid 工具 |
| 静态 ANSI / PNG | mermaid 技能 |
| 架构散文页面或演示文稿 | artifact 技能 |
/c4 <组件> 或“给我一个 X 的 C4 视图”是调用方式。该技能
能在它从未见过的仓库中工作:Scouts 是其了解任何信息的唯一途径,
因此这里没有任何东西依赖于对代码的先前知识。
步骤 0——确定组件
在任何 Scout 运行之前,陈述:
component: machine manager
repo root: /Users/aviral/modal
anchor: machine_manager/ (一个包,一个可部署单元)
- 组件 是读者可以在代码中指向的一件事:一个服务, 一个包,一个可部署单元。如果请求命名了一个跨越多个可部署单元的 子系统,请选择一个可部署单元并说明它引入了哪些邻居作为外部系统。
- 仓库根目录 是 Scouts 启动的地方。确认检出存在;不要 假设路径。
- 锚点 是最强的单一证据,证明该组件
是一个整体(包清单、Helm chart、Terraform 模块、proto
服务、
main.go)。如果你不能命名一个,那么该组件可能实际上是 两个组件——向人类询问而不是猜测。
最多问一个澄清性问题(组件边界是唯一值得问的问题)。 否则,做出决定并记录。
步骤 1——启动 Scouts
四个提示位于 prompts/ 中;每个都是一个完整的子智能体任务。将它们作为
subagent 调用启动,使用 agent: "scout"(只读侦察——Scouts
不得编辑仓库),所有四个在一个轮次中启动,以便它们并行运行,
cwd: 是步骤 0 中解析的仓库根目录,并且组件名称
替换 <component>:
| Scout | 提示 | 返回 |
|---|---|---|
| Scope | prompts/scope.md | 元素清单:id、一行角色、入口点、可部署单元、存储、外部服务、参与者、文件路径 |
| Relationships | prompts/relationships.md | 发起到 → 目标 : 动词 + 传输 行,每行带有 file:line |
| Boundary | prompts/boundary.md | 成员与外部系统 + 证明每个成员关系的模块/部署边界 |
| Levels | prompts/levels.md | 每个元素的层级分配,以及哪些层级必须拆分为 L1/L1b(L2/L2b) |
每个 Scout 返回带有 file:line 引用的证据。没有引用的行是
谣言;删除它或重新向该 Scout 请求来源。
从仓库根目录启动 Scouts——在那里运行会话(或者使
cwd: 与会话已经所在的目录相同)。cwd 与生成会话不同的 Scout
可能会在其进程启动前死亡,并且故障会显示为“其窗格中没有出现 pi 进程”。
不要轮询子智能体。结果会自动到达;当所有四个都返回时继续。 如果其中一个失败,请说明是哪个,并要么重试它,要么在生成记录中 记录这个缺口——永远不要从内存中填补空白。
步骤 2——协调成一个账本
将四个返回合并到生成记录中的三个表中。这就是 整个事实基础;规范可能不包含任何不在此处的内容。
elements: id | label | role | class (actor|external|member|store) | evidence
relations: id | from → to | verb + transport | level | evidence
boundary: id | inside/outside | justifying boundary | evidence
- 冲突 通过引用更强的来源来解决(清单或 部署图表优于注释;调用站点优于文档字符串),并且 失败的声明被记录,而不是删除。
- 未解决的冲突 和没有幸存来源的关系被记录为 开放问题。沉默是一个缺陷;可见的疑问不是。
- 每个元素保持一个 id 并在层级间重复使用,这样读者跟踪
q → reg在 L1 和 L2 看到相同的两个框。
步骤 3——绘制什么(优先级)
C4 视图是关于重要性的判断,而不是调用图的转储。 账本包含 Scout 找到的每个关系;图表包含读者需要的那些, 并且会明确说明它省略了什么。
当关系至少满足以下条件之一时,它获得一席之地:
| id | 测试 | 示例 |
|---|---|---|
| I1 | 它跨越组件边界(成员 ↔ 外部系统,外部系统 ↔ 外部系统) | webapi → Latitude API,machines → sshx.io |
| I2 | 它是源组件调用的主要请求、控制或数据路径(配置、心跳、审计、协调、启动) | control plane → private gRPC,River queue → workers |
| I3 | 它携带读者需要的协议或限定符:RPC 动词、传输、端口、存储桶、间隔、超时 | register + heartbeat every 1 min,gcore sync every 1 min |
| I4 | 它解释了读者无法从框中推断出的数据存储或参与者 | store → Postgres,ops + mmctl → webapi |
| I5 | 它是必须保持可计数的冗余或并行路径的一条分支 | machines → boot 和 boot → machines;两个独立的拉取 |
删除类别——关系仅作为以下类别之一从图表中移除:
| id | 类别 | 是什么 |
|---|---|---|
| C1 | 内部细节 | 完全在一个容器内部,不增加边界、协议或参与者信息(当存储的写入者已经绘制时,worker → store 每个查询的行) |
| C2 | 重复调用点 | 与已绘制关系相同的跳数,仅作业类型或参数不同,而已绘制标签已包含 |
| C3 | 辅助元素 | 其唯一工作是重新进入同一容器的元素(两个已绘制框之间的客户端包装器) |
| C4 | 冗余分支 | 一个路径的进一步并行分支,读者不需要其可数性(来自同一存储桶、同一传输的第二个工件) |
保持判断诚实的规则:
- 一个箭头可以承载多个调用点,当标签携带它们的
动词时(
create / terminate / repair)。这是 C2 折叠,而不是删除。 - 边界跨越在平局时获胜(I1 优先于 I3 优先于 I2)。一个为了给内部路径腾出空间而删除外部跳数的图表 已经颠倒了自身的优先级。
- 有疑问时,绘制它,并让层级的描述说明它为什么在那里。
疑问不是删除原因;
C1–C4是。 - 永远不要无声删除。 账本中的每一行都以以下状态之一结束:已绘制、
折叠到 <drawn relation>(折叠理由为 C2),或已省略 — C1|C2|C3|C4带一行原因。覆盖图(步骤 7) 说明四个计数:已绘制、已折叠、按类别省略、未说明。 - 范围缩减是不同的东西,需要调用者的话。
“仅控制平面视图”或“最多 20 个关系”是一个范围请求:将其记录为
基础(
basis: caller scope, N relations)以及计数的账本。优先级化是默认行为;子集 是调用者做出的决定, 而不是生成器推断的。
步骤 4——层级阶梯
目标形状是五个部分,从上到下:
L0 · 系统上下文 一个节点表示组件及其邻居
L1 · 组件 组件打开一次,容器包围其部件
L1b · <第二个侧面> 相同深度,系统的另一侧
L2 · 内部—<切片 A> 成员之间的关系,容器包围它们
L2b · 内部—<切片 B> 相同深度,另一个切片
这就是阶梯:一个五个部分的画布是可交付成果。更多部分 是一个缺陷,除非下面的拆分规范得到满足并有数字。
只在上述层级存在后才绘制每个层级,并在进入下一个层级前给出每个层级的描述。
| 层级 | 绘制内容 | 框行为 |
|---|---|---|
| L0 系统上下文 | 组件作为一个普通节点,加上周围的每个参与者和外部系统,以及它们之间的粗箭头 | 没有容器,没有 focus,没有双边框—重点是周围的世界,所以组件看起来像任何其他框 |
| L1 容器 | 组件打开一层:其自身的可部署/运行时单元在一个容器内部;L0 的外部系统保留在外部 | 容器框包围成员 |
| L1b 侧面视图 | 可选第二个切片,当一个 L1 确实无法容纳流程时(舰队端、数据平面、批处理路径)—有测量依据,绝不是新主题 | 容器,相同规则 |
| L2 组件 | 一个容器内部的关系—L1 的成员,现在相互调用 | 容器框包围它们 |
| L2b | 相同深度的可选第二个切片,当一个 L2 无法路由其关系时,或者两个故事需要分别排列时 | 容器,相同规则 |
不可协商的层级规则:
- L0 是组件作为一个普通节点。 整个组件一个框,
正常边框,加上周围的参与者和外部系统。
不是它的部件,不是它的监听器,不是它的表—那些是下一个层级。
如果 L0 有多个属于组件内部的节点,则它不是上下文视图。
L0 上的包含关系会声明读者尚未看到的内部内容,并且
focus会指向一个还不存在的层级。 - L1/L1b 打开 L0 的框。 每个成员位于容器内部; 每个在 L0 是外部的元素都位于容器外部。一个无法绘制在框的边界框外部的成员需要自己的容器条目 (参见连续性规则)—绝不是一个模糊的条目。
- L2/L2b 保持在一个框内部。 L2 关系的两个端点都是 容器成员。如果关系离开框,它属于 L1/L1b。
- 层级描述自身。 标题简短(
L0 · 系统上下文); 描述说明框内部是什么,框外部是什么,以及它如何与其相邻层级 关联。目标是 2-3 个包装行。 - 动效不是 C4 的属性。 层级 0-1b 是静态的(
dots: 0)。一个内部层级,其关系是实时流程,可以每条路线恰好携带一个点 (flow: true, dots: 1);记录这个选择。永远不要动画化外部箭头来装饰阶梯。
拆分规范——层级是深度,不是主题
一个 C4 层级在一个缩放级别回答一个问题。相同深度的两个层级 存在只是因为一个部分无法容纳内容;一个新的主题不是 添加部分的原因。
- 永远不要添加 L1c、L1d、L1e 或第五个侧面。 阶梯有三个 深度(上下文、容器、组件),并且在第一个深度以下最多在每个深度有两个 切片。一个随着每个子系统或每个提及而增加切片的画布是一个 戴着层级标题的主题列表。
- 第二个切片必须命名其度量标准: 路由
--check拒绝的路由,它报告的共享走廊,或冲突的排列(两个外部端没有共同的外部系统)。“感觉拥挤”不是一个度量标准。 - 相同深度的切片共享框、标签和 id。 L1 和 L1b 绘制相同的组件,使用相同的成员 id;L2 和 L2b 绘制相同的 成员。只有边集和排列不同,并且每个描述说明它属于哪个切片。
- 切片命名为
b(l1b,l2b);永远不要发明深度。 - 不绘制 L2 之外的深度。 如果组件内部仍然可分解, 在记录中说明并停止:代码类和函数不是读者要求的 C4 层级。 阶梯在请求结束的地方结束。
- 账本包含而阶梯未绘制的所有内容都是一个范围 决定,要写下来(步骤 6),而不是无声删除,也不是新部分。
步骤 5——规范机制
一个画布,一个阅读方向,每个层级一个部分:
{
"direction": "lr",
"theme": "gruvbox",
"diagrams": [
{
"id": "l0",
"title": "L0 · 系统上下文",
"description": "系统上下文:管理器是一个框。...",
"nodes": [ ... ],
"edges": [ ... ]
},
{
"id": "l1",
"title": "L1 · 管理器",
"description": "L0 的框打开一层:...",
"containers": [ { "label": "Machine manager", "members": ["api", "q", "db"] } ],
"layers": [ ["actor"], ["api", "q"], ["db"], ["prov"] ],
"nodes": [ ... ],
"edges": [ ... ]
}
]
}
direction: "lr"一次,在画布上。永远不要在单个图表中覆盖: 在每个部分都重新定向的读者会丢失阶梯。- 五个部分。
l0,l1,l1b,l2,l2b,按此顺序。第六个 部分需要记录步骤 3 的度量标准;十个或二十个部分的画布是 主题列表,而不是阶梯——将其折叠回三个深度,每个深度最多两个切片。 containers是按图表列出的列表,包含{label, members}。框是 围绕成员生长的真正双边框矩形,标签 嵌入其顶部边缘。每个图表允许多个框。- 成员 id 必须存在于该图表的
nodes中。 命名了不存在的节点的成员 id 会被解析器静默删除—然后框将围绕比声明的更少的节点绘制,并且--check不会警告。根据你声明的成员计数实际绘制的框。 - 连续性规则。 容器是一个矩形。
…
[因翻译需要已截断]