哪些 Agent 能力值得自己造

一、一个加功能要动四个文件的版本

我的项目里,最早那版「框架」是我自己都不想维护的东西。

那时候我刚学完怎么从零搭一个 Agent 框架,正是手痒的时候。于是我写了抽象基类,写了插件注册机制,给节点设计了继承体系,一切都很「框架」。然后需求来了:加一条新的业务链。

我得改路由逻辑、改基类、改节点注册、改报告渲染,一共四个文件。加第二条的时候,还是四个文件。

问题不在于我写得不够好,而在于我造错了地方。那些抽象基类和插件机制,解决的是「怎么把代码组织得像个框架」,可我真正要解决的是「怎么让加一条业务链变得便宜」。这是两件事。

后来我把大部分抽象砍掉了,重新想了一遍哪些东西值得我亲手造、哪些根本不该我造。现在加一条业务链的成本是:往一张表里加一条记录,再实现对应节点。路由逻辑一行都不用动。

这篇写的就是这个「砍掉重来」的过程,以及它背后那条我认为最重要的判断标准。

二、先交代这个系统在做什么

后面所有例子都来自同一个项目,先花一段话把它说清楚,不然那些「业务链」「模型族」会一直悬在空中。

这是一个面向县域经济分析的智能体系统。你可以把它想成一个「会写分析报告的助手」:使用者(通常是研究人员或政府部门)提一个问题,比如「A 县今年的产业发展有什么瓶颈」,系统调度一批「专家角色」,有人负责调数据、有人负责跑统计模型、有人负责把结论写成政策建议,最后产出一份带证据的报告。

它不是聊天机器人。它的每一次运行都对应一条相对固定的「工作流程」:先做什么、再做什么、谁来做、做完输出什么、要不要人工复核,都是提前规定好的。这类系统在业内一般叫 workflow 型 Agent(流程编排型),区别于那种让大模型自己天马行空决定下一步的 autonomous 型 Agent(自主决策型)。

这一点很关键:这个系统的大部分「决策」是我用确定性代码写死的,不是交给大模型现场发挥的。整篇文章的设计,几乎都是这个选择的延伸。

三、先把几个词说成人话

这篇会反复出现几个词。初学者最容易卡在这里,我先用最朴素的说法过一遍,后面看到就不慌了。

  • 运行时 / 编排 / 领域:一个 Agent 系统从下到上大致分三层。最下面是「怎么跟大模型说话、怎么执行工具」,中间是「多个步骤按什么顺序跑」,最上面是「在我的业务里,什么算合法、该走哪条流程」。下一节有张表专门讲它们。
  • 注册表(registry):说白了就是一张清单,或者说一份配置表。与其把「有哪些业务链、每条链怎么跑」写成一堆 if-else 散在代码里,不如集中列成一张表,程序去查表办事。加能力就是往表里加一行。
  • 契约(contract):一段机器可读的「准入条件」声明。比如「跑聚类模型,至少要 5 个县、2 个指标」,把这句话写成程序能检查的数据结构,就是契约。它规定了够不够格进入下一步。
  • 门禁(gate):真正拿契约去卡的那道关。数据不够、章节缺失,就在这里被拦下,不让往下走,也不出正式结论。
  • 状态黑板(state):LangGraph 这类编排框架里,各个步骤之间靠一个共享的大字典传递信息,上一步把结果写进去,下一步从里面读。像一块所有人都能写、都能看的黑板。本文里 state 就是它。
  • trace(痕迹,执行记录):每一步都往一个列表里记一笔「我是谁、干了什么、成没成」。出问题时,顺着这条痕迹就能复盘,不用靠猜。
  • 声明式(declarative):只描述「要什么」,不写「怎么一步步做」。注册表和契约都是声明式的:你声明「这条链需要这些输入、派这些角色」,至于程序内部怎么调度,是另一回事。

这几个词是全文的骨架,尤其「注册表 + 契约」这对组合,是我这层框架的全部核心。

四、先把”造框架”说清楚

这篇不是要劝你一开始就做一个框架。对初学者来说,最好的顺序通常是:先用现成 SDK 写通一次调用,再用现成编排框架跑通工作流,最后只在重复痛点出现后再抽象。

前面提到的三个层次,先记住它们各自的职责:

层次 它解决什么 常见例子
运行时层 怎么调用模型、传消息、执行工具 LLM SDK、MCP、模型提供商适配
编排层 多个步骤按什么顺序运行 LangGraph 的节点、边与状态
领域层 在你的业务里什么算合法、该走哪条流程 业务链、数据门槛、人工复核规则

一句话记法:运行时管「怎么说话」,编排管「按什么顺序走」,领域管「在我这行什么才算对」。前两层是所有 Agent 系统都长得差不多的部分,第三层才是每个业务各不相同的部分。

后文的项目案例只是在说明:当领域规则已经稳定且重复出现时,为什么我选择在第三层做一层很薄的抽象。

五、教科书里的自建框架长什么样

先把参照系立起来。一个教学向的 Agent 框架,通常会包含这几层。

1. LLM 抽象层。 统一多个提供商(OpenAI、本地 vLLM、Ollama……)的调用差异,甚至能自动探测 provider。

2. Message 类。 统一消息格式,用 Literal 把 role 限死在四个取值上,对内带 timestamp 和 metadata,对外 to_dict() 转成 OpenAI 兼容格式。

1
2
3
4
5
6
7
8
9
10
MessageRole = Literal["user", "assistant", "system", "tool"]

class Message(BaseModel):
content: str
role: MessageRole
timestamp: datetime = None
metadata: Optional[Dict[str, Any]] = None

def to_dict(self) -> Dict[str, Any]:
return {"role": self.role, "content": self.content}

3. Config 类。 把硬编码的配置集中起来,支持 from_env() 从环境变量覆盖。

4. Agent 抽象基类。abc 定义统一接口,强制子类实现 run()

1
2
3
4
5
6
7
8
9
10
11
class Agent(ABC):
def __init__(self, name, llm, system_prompt=None, config=None):
self.name = name
self.llm = llm
self.system_prompt = system_prompt
self.config = config or Config()
self._history: list[Message] = []

@abstractmethod
def run(self, input_text: str, **kwargs) -> str:
...

5. 范式框架化。 把 ReAct、Reflection、Plan-and-Solve 这几种「Agent 的思考套路」改造成继承同一基类的组件。ReAct 是「边想边做、想一步做一步」,Reflection 是「做完自己回头挑错再改」,Plan-and-Solve 是「先列计划再逐条执行」,都是让大模型更靠谱的常见范式,这里不展开。

6. 一个统一抽象。 比较激进的做法是「万物皆为工具」,把 Memory、RAG、MCP 全都抽象成 Tool,消除多余的概念层。

这套设计的自洽性很好,作为学习路径也非常清晰。自建框架的理由通常有三条:通用框架过度抽象、迭代太快导致 API 不稳、实现黑盒化难以定制。这三条我都认,但它们在我的项目里指向了完全不同的结论。

六、我实际造了什么,没造什么

我的项目底层跑在一个现成的 Agent 运行时上,工作流引擎用 LangGraph。把整个系统按层摊开,界线其实非常清楚:

规律很直白:凡是通用的我都没造,凡是领域的我都造了。

理由很朴素。我造一个 LLM 适配层,能比现成的好吗?大概率不能,我只会写得更少、bug 更多、还得自己维护。但「一份县域经济分析报告在什么条件下才算合格」这件事,市面上没有任何框架知道。这才是只有我能写的部分。

教科书说「通用框架过度抽象」,这话对;但结论不该是「所以我从零再造一个通用框架」,而应该是:在通用框架之上,造一层贴合我领域的薄抽象。

七、我的框架:注册表 + 契约

那么这层「领域框架」长什么样?核心就两个概念。下面几节我会先给零件,第八节再把一个真实请求从头到尾串一遍,你就能看到它们怎么协作。

7.1 业务链注册表:能力即配置

系统里有近十条业务链(运行监测、产业诊断、瓶颈归因、政策决策、项目招商、模型分析、数据治理……)。它们不是十段 if-else,而是一张注册表里的十条声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WORKFLOW_CATALOG: list[dict[str, Any]] = [
{
"workflow_id": "model_analysis",
"name": "统计分析与计量模型研判",
"business_chain": "模型分析链",
"trigger_phrases": ["模型分析", "计量模型", "因果推断", "回归", "预测", ...],
"required_inputs": ["county_code", "period"],
"optional_inputs": ["model_family_id", "target_metric", "feature_metrics", ...],
"roles": ["chief_economist", "model_analyst", "report_editor"],
"tools": ["load_dataset", "run_model", "render_model_report"],
"outputs": ["model_analysis_report_path", "model_analysis_result_path"],
"human_review_policy": "required_before_policy_circulation",
},
# ... 其余业务链
]

一条声明就说清了:怎么被触发、需要什么输入、派哪些角色、能用哪些工具、产出什么、要不要人工复核。

关键是查表这一步长什么样。整篇文章的地基就在这几行,路由不做判断,它只是拿用户的问题去表里找命中的那条:

1
2
3
4
5
6
7
8
9
def route_workflow(question: str) -> dict[str, Any]:
for spec in WORKFLOW_CATALOG:
if any(phrase in question for phrase in spec["trigger_phrases"]):
return {"workflow_id": spec["workflow_id"],
"reason": "trigger_match",
"spec": spec}
return {"workflow_id": "general_monitoring", # 兜底
"reason": "default",
"spec": _default_spec()}

看清楚它做了什么、没做什么:它没有问大模型「这个问题该走哪条链」,只是做了一次确定性的关键词匹配,然后把整条声明 spec 原样带出来。后面的调度、派角色、挂工具,全都读这个 spec,不再有第二处「知道业务链长什么样」的代码。

新增一条业务链,主要工作是往这张表里加一条记录,再实现对应节点,而不是去改路由逻辑。路由逻辑本身一行都不用动,因为它只认这张表。这就是「把『加功能』变成『加数据』」最直接的一次兑现。

7.2 角色注册表:角色是文件,不是代码

系统里的「专家角色」(首席经济学家、政策策划专家、规划专家、报告编辑……)没有一个是 Python 类。它们都是带 frontmatter 的 markdown 文件(frontmatter 就是文件顶部两条 --- 之间那段结构化配置,写博客的人应该很熟):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
role_id: policy_planner
name: 一县一策政策策划专家
business_chains:
- 政策决策链
- 一县一策与规划链
default_tools:
- search_policy
- query_expert_rules
outputs:
- policy_needs
- policy_action_options
confidence_policy: evidence_required
---

# 角色定位

你负责把运行监测、产业诊断和瓶颈分析中的问题,转化为可研究、可协调、可纳入规划的政策抓手。
...

上半段的 frontmatter 是给程序看的配置,下半段的正文是给大模型看的提示词。加载器就是几十行:解析 frontmatter,body 当提示词。

1
2
3
4
5
6
def load_role(role_id: str) -> dict[str, Any]:
path = _role_path(role_id)
meta, body = _parse_frontmatter(path.read_text(encoding="utf-8"))
meta["body"] = body # 正文 → system prompt
meta["path"] = str(path)
return meta

于是第 7.1 节里 spec["roles"] 那几个 id,到这里就变成了一份份具体的角色配置:程序读 frontmatter 知道「这个角色能用哪些工具、该产出什么」,大模型读正文知道「我该以什么身份、什么口吻思考」。一份文件同时喂给了代码和模型两边,这是这个设计最舒服的地方。

它还带来几个我做完才体会到的好处:

  • 业务专家可以参与维护。调整角色的措辞和边界通常不需要改 Python;但 frontmatter 仍应经过字段校验、评审和版本控制,避免把错误配置直接带进生产流程。
  • frontmatter 是机器可读的契约。business_chains 决定它属于哪条链,default_tools 限定它能用什么工具,confidence_policy: evidence_required 声明了它「必须有证据才能下结论」,这些都能被代码检查。
  • 版本控制天然可用。角色的每一次修改都是一次 diff。

有意思的是,这个「markdown + frontmatter」的模式并不是我发明的。现在主流 Agent 工具的技能(skill)和子代理定义,几乎都是这个形状。能力的定义正在从代码往声明式文件迁移,我只是撞上了同一个答案。

7.3 模型族:一个完整的小框架

这是我最花心思的一块,也是最能说明「注册表 + 契约」价值的例子。

系统要支持多种统计/计量模型:综合评价、聚类、回归、因果推断、时序预测、异常检测、空间计量……如果每来一种就写一个节点,很快就会失控。所以我把它做成了一个小框架,六个部件各司其职:

这六个部件里,只有 executor 装着真正的算法。其余五个回答的都是「谁能跑、要什么、够不够、算完怎么呈现」,全是编排与约束,而这恰恰是每加一个新模型族时最容易写重复的部分。

契约长这样,注意 minimum_shape,它把「多少数据才算够」写成了机器可读的声明:

边界说明: 这里的 minimum_shape 只是”允许流程进入下一步”的最低工程门槛,不是统计结论已经可靠的证明。特别是因果推断,还需要另行检查识别假设、对照组可比性、样本量与稳健性;不满足时系统应拒绝输出正式因果结论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MODEL_INPUT_SPECS = {
"<综合评价模型族>": {
"name": "多指标综合评价模型",
"required_request_fields": [],
"optional_request_fields": ["model_method", "feature_metrics", "metric_weights", ...],
"minimum_shape": {"county_count": 2, "period_count": 1, "metric_count": 1},
},
"<聚类模型族>": {
"name": "聚类分析模型",
"required_request_fields": ["feature_metrics"],
"minimum_shape": {"county_count": 5, "period_count": 1, "metric_count": 2},
},
"<因果推断模型族>": {
"name": "因果推断模型",
"required_request_fields": ["target_metric", "treatment_field", "policy_time"],
...
},
}

门禁就是拿这份契约去卡的那道关,逻辑朴素到几乎不用解释,但它是「确定性代码替大模型把关」的典型:

1
2
3
4
5
6
7
def quality_gate(family_id: str, dataset_shape: dict) -> dict:
need = MODEL_INPUT_SPECS[family_id]["minimum_shape"]
missing = {k: (need[k], dataset_shape.get(k, 0))
for k in need if dataset_shape.get(k, 0) < need[k]}
if missing:
return {"passed": False, "status": "needs_data", "missing": missing}
return {"passed": True}

数据不够,它就返回 needs_data,并说清差在哪个维度、差多少,流程到此为止,不出正式结论。这件事你不能指望大模型自觉,它高兴起来 3 个样本也敢给你讲因果。把关得是代码的事。

路由依然是确定性的关键词匹配,而且这点我很得意:它会把「为什么这么选」一起返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def route_model_family(request: dict[str, Any]) -> dict[str, Any]:
# 1. 显式指定优先
if request.get("model_family_id"):
return {"model_family_id": ..., "reason": "request_override", ...}

# 2. 关键词匹配
question = str(request.get("question", ""))
for model_family_id, terms in ROUTE_TERMS:
if any(term in question for term in terms):
return {
"model_family_id": model_family_id,
"reason": "keyword_match",
"matched_terms": [t for t in terms if t in question],
"family": get_model_family(model_family_id),
}

# 3. 兜底
return {"model_family_id": "<默认模型族>", "reason": "default", "matched_terms": []}

reasonmatched_terms 这两个字段不参与任何计算,纯粹是为了可解释。出了问题,一眼就能看出是用户指定的、关键词命中的、还是兜底兜出来的。这是我在调试中被坑过之后加的:当路由是个黑盒时,排查成本高得离谱。

现在新增一个模型族要做什么?往 catalog 加一条、往 input_specs 加一条契约、往 ROUTE_TERMS 加一组关键词、在 executor 里实现算法。路由、门禁、报告渲染全都不用动。

7.4 产物注册表与任务状态机

产物(报告、数据集)也是注册表:

1
2
3
4
5
6
7
8
9
ARTIFACT_SPECS: dict[str, dict[str, str]] = {
"monitoring_report_path": {"name": "运行监测报告", "kind": "markdown_report"},
"industry_diagnosis_path": {"name": "产业诊断报告", "kind": "markdown_report"},
"policy_action_matrix_path": {"name": "政策行动矩阵", "kind": "json_dataset"},
"model_analysis_result_path": {"name": "模型分析结果", "kind": "json_dataset"},
# ...
}

JOB_STATUSES = {"draft", "queued", "running", "succeeded", "failed", "needs_human_review", "ok"}

任务状态则是从状态黑板推导出来的,而不是让节点各自去设置,也就是保持单一事实来源:

1
2
3
4
5
6
7
8
def _derived_status(state) -> str:
if state.get("decisions", {}).get("requires_human_review"):
return "needs_human_review"
if state.get("review", {}).get("issues"):
return "needs_human_review"
if state.get("review", {}).get("passed"):
return "succeeded"
return "draft"

这个小函数避免了一类很讨厌的 bug:某个节点忘了把任务标记成待复核,结果有问题的产出被当成成功的发出去了。「让每个节点自己记得设状态」是一种极易出错的约定,只要有一个节点漏了就出事。现在状态是算出来的,不是设出来的,它只看黑板上的事实(有没有要求复核、复核有没有挑出问题、有没有通过),漏标记这件事从根上没了。

7.5 节点契约

最后是最简单也最重要的一条约定:每个节点都是 state -> state 的普通函数,并且必须往 trace 里记一笔。

1
2
3
4
5
def some_node(state: CountyEconomyState) -> CountyEconomyState:
result = do_work(state)
state.setdefault("analysis", {})["xxx"] = result
add_step(state, "some_node", "ok", {"meta": ...}) # 契约:留下痕迹
return state

「节点是 state -> state 的函数」这句话值得多看一眼:它读黑板、往黑板上写、再把黑板交给下一个节点。整条工作流就是一串这样的函数首尾相接,LangGraph 负责按图把它们串起来。没有基类,没有装饰器,没有元编程,契约靠约定和测试保证,不靠继承。

我一开始真的想过写个 BaseNode 抽象基类,后来放弃了。它除了增加一层间接,什么也没带来。节点之间的共性其实只有读状态、写状态、记 trace 三件事,用一个 add_step 辅助函数就够了。

能用函数解决的,就别上类;能用约定解决的,就别上继承。

八、把零件串起来:一个请求从头走到尾

前面拆了一堆零件,初学者最容易在这儿丢掉全局。所以这一节我们只做一件事:拿一个真实问题,跟着它从进到出走一遍,看每个零件在哪一步接手。

假设用户问:「帮我用因果推断评估一下 A 县那项产业补贴政策有没有效果。」

第 1 步,命中业务链。route_workflow(question) 拿这句话去 WORKFLOW_CATALOG 里扫 trigger_phrases,「因果推断」命中了 model_analysis 这条链。返回的 spec 里写着:需要 county_codeperiod,派 chief_economist / model_analyst / report_editor 三个角色,产出两份产物,且 human_review_policy 要求「进入政策流转前必须人工复核」。到这里,「这次要干什么」已经完全确定,而且没问过大模型。

第 2 步,装配角色。调度器读 spec["roles"],对每个 id 调 load_role,把 frontmatter 里的工具清单挂上、正文当 system prompt。model_analystconfidence_policy: evidence_required 在这一步就生效了,它注定不能凭一句话下结论。

第 3 步,选模型族。进到模型分析节点,route_model_family 再扫一次关键词,「因果推断」命中 <因果推断模型族>reason="keyword_match" 一并记下。

第 4 步,过门禁。系统把 A 县的数据整理成 dataset_shape,交给 quality_gate 对着 minimum_shape 检查。因果推断对样本要求高,假设这次只凑到 3 个对照县、门槛要 8 个,quality_gate 返回 needs_data,明说「county_count 差 5」。流程在这里停住,不出正式因果结论,只回一份「数据不足、需要补哪些」的说明。这正是第七节那句「不能指望大模型自觉」的地方,把关是代码干的。

第 5 步(假设数据够了),执行与渲染。门禁放行,executor 跑真正的算法,结果写回状态黑板,report 节点按 ARTIFACT_SPECS 的声明渲染成报告,产物落盘。

第 6 步,推导状态。全程每个节点都 add_step 记了痕迹。最后 _derived_status 看黑板:因为这条链 human_review_policy 要求复核,requires_human_reviewTrue,于是任务状态被算成 needs_human_review,它不会被误标成 succeeded 直接发出去。

走完这一趟你会发现:大模型只在「角色内部思考、生成文字」时出场,所有「走哪条路、够不够格、算不算成功」的骨架决策,都是确定性代码定的。这就是这套设计的脾气。

九、框架化真正的回报:可测试性

框架化最大的收益,我原以为是「代码整洁」,实际是可测试性。

在我写作时,这个项目的领域层大约 7500 行代码、33 个测试文件。这不是外部基准,而是一个项目内部的可维护性记录;能测得比较密,靠的正是前面那些设计。为什么这些设计就可测?因为第八节里那些骨架决策全是纯函数,给定输入必得同样输出,不依赖大模型、不依赖网络。纯函数是最好测的东西。举个最直白的例子,路由测试长这样:

1
2
3
4
5
6
7
8
9
10
def test_route_workflow_causal_inference():
r = route_workflow("帮我用因果推断评估这项补贴政策的效果")
assert r["workflow_id"] == "model_analysis"
assert r["reason"] == "trigger_match"

def test_quality_gate_blocks_insufficient_data():
r = quality_gate("<因果推断模型族>", {"county_count": 3, "period_count": 4, "metric_count": 5})
assert r["passed"] is False
assert r["status"] == "needs_data"
assert "county_count" in r["missing"] # 明确指出差在哪一维

没有 mock、没有起服务、没有调 API,就是普通的输入到断言。整个领域层的测试大体都是这个形状:

  • 注册表可测:WORKFLOW_CATALOG 的结构、每条业务链的必填字段完整性,都是纯数据校验。
  • 路由可测:给一个问题,断言它命中哪个 workflow_id 或模型族。因为路由是确定性的,测试不需要 mock LLM。
  • 契约可测:input_specs 的校验逻辑、minimum_shape 的判定,全是纯函数。
  • 门禁可测:给一份缺章节的草稿,断言它必须被拦下来。
  • 状态可测:_derived_status 是纯函数,各种组合直接断言。

这就串起来了:因为把决策权从 LLM 手里收回到了确定性代码,所以这些逻辑才可测;因为可测,所以敢改。一个到处是 LLM 自由决策的系统,是没法写出 33 个稳定测试文件的,你连断言什么都不知道,因为同一个输入它每次输出还不一样。

从框架化到声明式,再到确定性、可测试,这条链是通的。

十、几点教训

1. 「万物皆为工具」很优雅,但我的领域里是「万物皆为注册表条目」。 教科书那个统一抽象(把 Memory、RAG、MCP 都变成 Tool)在教学上极有说服力。但我的系统里,需要被统一的不是「能力」而是「声明」:业务链、角色、模型族、产物,各自都是注册表里的一条,工具只是其中一类。统一抽象要选对被统一的那个东西,选错了就会到处别扭。

2. 我一开始过度设计了。 最早的版本里我写了抽象基类、写了插件机制、想做得很「框架」。结果是每加一个功能要动四个文件。后来砍掉大部分抽象,回到「普通函数 + 数据声明」,反而清爽了。抽象的合理数量,取决于你真的有多少个实现,而不是你觉得未来可能有多少个。只有两三个实现的时候就写基类,你是在为一个还没到来的未来付利息。

3. 领域框架有个明确的检验标准。 怎么判断这层框架造对了?看加一个新能力的成本。如果加一条业务链要改路由、改状态、改报告渲染,那这层框架是失败的;如果只是加一条声明加一个节点,那它就在干活。这个标准比「代码看起来优不优雅」靠谱得多,优雅是感觉,改动成本是能数出来的。

4. 别造你能直接用的东西。 这是最省时间的一条。我完整学过怎么手写 LLM 客户端、Message、Agent 基类,学是值得的,因为出问题时我知道底下在发生什么;但生产上再写一遍,纯属浪费。理解和重写,是两件事。

十一、小结

回到开头。「构建你自己的 Agent 框架」这件事,我的答案是:

  • 该造的是领域层:业务链注册表、角色注册表、模型族的契约与门禁、产物注册表、节点契约。这些没人能替你造,因为它们编码的是只有你懂的业务约束。
  • 不该造的是 LLM 抽象、消息格式、对话循环、工具执行、图编排。这些是标准件,直接用。

而这层领域框架的核心,说白了就一句话:把「加功能」变成「加数据」。能力用声明表达,路由靠查表,合法性靠契约检查,痕迹靠约定留下。这样系统才长得大、测得动,也才敢让别人来改。

教科书教会我的是框架里每一层在干什么,这个理解是我后来敢于「大部分都不造」的底气。看懂了才知道哪层不用自己写,这个顺序不能反:先看懂全貌,才谈得上有取舍地省略。


参考资料