一套讲清"复杂系统怎么从不同角度看"的方法
顺便把 类图 / 时序图 / story 各自的位置一次说明白
→ 按 → 键 / 点右下箭头 / 手指左滑 翻页
同一个系统,不同人关心完全不同的侧面:程序员想知道代码怎么分包,运维想知道跑在哪台机器,用户只关心"它到底给我带来什么"。硬要用一张图全部说清,结果是谁都看不懂。
Philippe Kruchten 的主意很简单:别强求一张图。把架构拆成 4 个视图,各自只回答一类人的问题;再用 1 组场景(story) 把它们串起来、互相验证。这就是 4+1 View Model。
下面每一页,上半讲这个视图是什么、解决谁的问题;下半用 xskill 的真实结构做例子。xskill 我们按三个模块来讲 👇
① 轨迹的入口与索引 ② 轨迹生成 Skill ③ 轨迹的评价与推送
四角是四个视图,中间的「场景」是把它们黏在一起的那个 +1。
系统由什么构成:实体、职责、关系 → 用类图
运行时怎么动:时间顺序、并发 → 用时序图
几条关键用户故事,驱动设计、最后验收,把上下左右四个视图串起来
代码怎么组织:包、模块、依赖 → 用包图
软件跑在哪:进程、机器、网络 → 用部署图
xskill 做的事:把 coding agent 的轨迹(trajectory)自动炼成可复用的技能(skill)——"一个人解决,人人复用"。这条流水线分三段:
从各家 agent(Claude Code / Codex / Trae …)捕获对话轨迹,桥接成统一 traj_*.md,再切成原子任务、建索引。让原料进得来、找得到。
把同类原子任务聚类、归并,蒸馏出一个 baby skill(SKILL.md + git 分支)。把零散经验炼成成型技能。
新技能先走灰度(canary),收 ux_score 决定 promote / reject;通过后安装/推送给所有人与所有生态。让好技能经得起检验、传得出去。
场景视图就是几条关键用户故事 / 用例。它不是"第五个并列视图",而是把其余四个视图串起来、驱动设计、最后用来验收的那根线。Kruchten 把它画在正中间,是有意的——story 是圆心。
这条 story 同时说明了价值、又规定了四个视图必须支撑什么。你写的 story,就是在写这个 +1。
逻辑视图回答"系统由什么构成":有哪些核心实体、各自职责、谁拥有谁、谁继承谁。它的主力表达工具就是 类图(class diagram)——一张静态快照,不关心时间。
classDiagram
class Ingester {
+scan_and_bridge()
}
Ingester <|-- TraeIngester
Ingester <|-- JsonlIngester
Ingester <|-- SqliteIngester
class Trajectory {
+traj_id
+session_id
+markdown
}
class DirectoryWatcher {
+scan_once()
+state_machine
}
class AtomTask {
+atom_id
}
class Skill {
+name
+SKILL_md
+branch_baby_main
}
class Canary {
+ux_score
+promote_or_reject()
}
Ingester --> Trajectory : produces
DirectoryWatcher --> Trajectory : consumes
DirectoryWatcher --> AtomTask : splits
AtomTask --> Skill : cluster_merge
Skill --> Canary : canary
抽象 Ingester 派生出 Trae/Jsonl/Sqlite 三种——新增一家 agent 只是多一个子类。
进程视图回答"运行时怎么动":消息按什么时间顺序流转、谁和谁并发、轮询与状态怎么推进。主力工具是 时序图(sequence diagram)。它也常用来把某一条 story 展开成具体运行轨迹——所以时序图是 story 通往进程视图的桥。
sequenceDiagram
autonumber
participant Bob as Agent(Bob)
participant I as Ingester ①
participant W as Watcher ②
participant S as Skill 仓库 ②
participant C as Canary ③
participant Al as 同事 Alice ③
Bob->>I: 产生轨迹 (vscdb/jsonl)
I->>W: 桥接 traj_*.md (discovered)
W->>W: split → index → cluster
W->>S: 归并出 baby skill
S->>C: 达阈值 → 进 staging 灰度
C->>Al: 按概率分流试用
Al-->>C: 回传 ux_score
C->>S: 收齐样本 → promote 到 main
S->>Al: install 到 ~/.claude/skills 等
同一条 story(第 5 页),在这里变成了带时间轴、可落地的交互序列。
开发视图回答"代码怎么组织":源码分成哪些包/模块、谁依赖谁。它服务的是程序员,关心的是可维护、可分工。三个功能模块会落到具体的包上。
① ecosystems/pipeline · ② skill · ③ team + canary。视图四看的是"文件夹",不是"运行"。
物理视图回答"软件跑在哪":进程怎么分布到机器、走什么网络、装到哪个目录。它服务运维。同一套逻辑,可以有不同的物理部署。
一个 t2s serve 进程,自己捕获①、自己蒸馏②、自己灰度③,skill 直接 install 到本机生态目录 ~/.claude/skills / ~/.trae-cn/skills。
client 的 collector 捕获并上传轨迹 → server 集中蒸馏+灰度 → 再下发给组织里每个 client。前面有 nginx,跑在 Docker 里。同样的三模块,换了张部署图。
它们不是三个并列的"图",而是各自挂在不同视图上的表达工具:
🥥 记忆法:story 给出题目,类图说明"长什么样",时序图说明"怎么跑起来"。
| 视图 | 回答的问题 | 工具 | xskill 例子 |
|---|---|---|---|
| 场景 +1 | 这东西带来什么? | story / 用例 | Bob 解决 → Alice 复用 |
| 逻辑 | 由什么构成? | 类图 | Ingester · Skill · Canary |
| 进程 | 运行时怎么动? | 时序图 | discovered→…→done→灰度 |
| 开发 | 代码怎么组织? | 包图 | ecosystems/pipeline/skill/team |
| 物理 | 跑在哪? | 部署图 | standalone vs team(C/S) |
写 xskill 的 story 时:先用场景(+1)把主线讲清,再分别用类图(逻辑)和时序图(进程)把"长什么样 / 怎么跑"补齐,开发与物理视图交代给程序员和运维。🥥
↓ 继续翻 · 深入「模块① 轨迹的入口与索引」的 4+1 →
把「① 这一段」单独拉出来,完整走一遍它的 4+1
负责从「原料产生」到「原料就绪」:从各家 agent 落盘的轨迹开始,到每条被切成原子任务、建好索引为止。状态机上 owns discovered → indexed;之后的 cluster→done 交给模块②。
agent 产生轨迹 → traj_*.md → [indexed] → ②cluster…
先讲清「①要支撑哪几条故事」,其余四视图都为它服务。
Claude Code / Codex / Trae / OpenCode…… 不管哪家,Bob 干完活的轨迹都要能被捕获,统一成一种格式。
轨迹进来后无需人工:自动切原子任务、建索引,静静等蒸馏。Bob 不做任何额外操作。
daemon 重启、轮询重跑,同一条轨迹不被重复摄入;新轨迹要被及时发现。
空的/损坏的/不成功的轨迹被识别并 filtered,不污染后面的技能。
两层:入口层 ecosystems/(每家 agent 一个适配器) + 索引层 pipeline/。两层通过「桥接目录 + 统一格式」解耦。
classDiagram
class Ingester {
+scan_and_bridge()
+start()
+stop()
}
Ingester <|-- JsonlIngester
Ingester <|-- SqliteIngester
Ingester <|-- CCSessionIngester
Ingester <|-- TraeIngester
class Trajectory {
+traj_id
+session_id
+markdown
}
class DirectoryWatcher {
+scan_once()
}
class AtomTask {
+atom_id
}
class AtomTaskStore {
+index_pkl
}
class Registry {
+trajectories_table
}
Ingester --> Trajectory : bridge
DirectoryWatcher --> Trajectory : discover
DirectoryWatcher --> AtomTask : split
AtomTask --> AtomTaskStore : store
DirectoryWatcher --> Registry : status
两个独立循环:Ingester 各自轮询桥接 / Watcher 线程池推进 split+index。
sequenceDiagram
autonumber
participant AG as Agent 本地目录
participant IN as Ingester
participant BR as 桥接目录
participant W as DirectoryWatcher
participant R as Registry / Store
AG->>IN: 落盘原生轨迹
IN->>IN: dedup(seen) 去重
IN->>BR: 写 traj md + json(meta)
W->>BR: _scan_once 轮询发现
W->>R: discover → discovered
W->>W: split → 拆 AtomTask
W->>R: split_done
W->>W: embed → index.pkl
W->>R: indexed(交给模块②)
flowchart LR
D[discovered] -->|split| S[splitting] --> SD[split_done] -->|embed| I[indexed]
D -->|校验不合格| F[filtered]
S -->|异常| E[error] -->|retry 带计数| D
I --> C["② clustering …"]
style I fill:#E2F1F2,stroke:#0E7C86
style C fill:#FCE5DC,stroke:#E0613B
去重(S3):内存 seen + 重启 _scan_seen_sessions 从落盘 .json 重建。僵尸回滚:重启时 in-flight 中间态(splitting/clustering)无对应 future → 退回前一阶段重调度,避免卡死。脏数据(S4):validate_trajectory_source 不合格 → filtered。
依赖方向:pipeline 不依赖具体 agent;ecosystems 只负责「产出统一 traj_*.md」——这正是类图里那条 produces/consumes 边界。
Claude Code ~/.claude/projects/**.jsonl
Codex ~/.codex/sessions/
OpenCode/ngagent 本地 SQLite
Trae state.vscdb + CLI trajectory_*.json
桥接 ~/.xskill/<eco>_sessions/traj_*.md
原子任务 <traj_id>/tasks/
向量索引 index.pkl(落 watch_dir)
真相源:registry 的 SQLite
t2s serve 一个进程,本机捕获 + 本机索引,全在一台机器。
client 的 collector 起 ingester,把桥接出的 traj 按 traj_id→sha256 上传 server;索引/蒸馏在 server 侧。同一套入口逻辑,换了部署位置。
| 视图 | ① 里是什么 |
|---|---|
| 场景 +1 | S1 多平台接入 · S2 自动就绪 · S3 不重不漏 · S4 脏数据挡门外 |
| 逻辑 | Ingester 家族 → Trajectory → Watcher → AtomTask/Store → Registry |
| 进程 | 双循环:轮询桥接 / 状态机 discovered→splitting→split_done→indexed |
| 开发 | ecosystems/(入口) + pipeline/{registry,trajectory,atom,runner}(索引) |
| 物理 | 读各 agent 本地目录 → 桥接目录 → index.pkl;standalone / 上传 server |
模块① 完。模块②(生成 Skill) 与 模块③(评价与推送) 将照同一结构展开。🥥
← 回头再看