普通视图

发现新文章,点击刷新页面。
昨天以前首页

Claude Code Harness 工程:数仓侧落地方案|得物技术

作者 得物技术
2026年5月21日 09:56

一、AI Coding 现状与痛点:为什么需要 Harness

当前使用情况

得物离线数仓各小组已基本完成 AI Coding 工具的覆盖,主力工具为 Claude Code,辅以数据平台的 IDE 插件,应对重复性工作时效率提升明显。

核心痛点

尽管整体提效已显现,但团队在实际使用中暴露出三类结构性痛点。

痛点一:AI 不记得上下文约束,开发过程中反复"失忆"。会话开始时告知了"金额字段单位是千元",对话进行到一半后 AI 忘了,生成的 SQL 把千元当元用,导致数据差了 1000 倍。这不是偶发问题,而是 Claude Code 的 context compact 机制(上下文压缩)的系统性限制:当对话 token 接近上限(约 95%)时,历史内容被自动压缩为摘要,临时口头说的约束全部丢失。

痛点二:规范执行不稳定,靠记忆兜底的部分最容易出问题。OneData 命名规范、注释三段式、INSERT 必须带 PARTITION 子句……这些规范大家都知道。在项目工期紧张时,人工规范遵守率降至 60%~70%,AI 靠 prompt 记忆的规范遵守率也只有 70%~80%。真正需要的是:把规范从"LLM 记忆中的指导性内容"变成"每次执行时强制检查的护栏"。

痛点三:大型需求开发中,context 很快被撑满,越到后期 AI 越不可靠。复杂需求(大型宽表、大量下游血缘)的典型开发过程:

血缘查询结果(500~3000 tokens) 自测 23 条 SQL 执行结果(5000~15000 tokens) SKILL 规范文件内容(~10000 tokens) 数据比对两表样本(大量行)= context 迅速膨胀 → compact 触发 → 关键约束遗忘 → AI 开始犯低级错误

核心矛盾:越是复杂的需求,越依赖 AI;但越复杂的需求,context 越容易撑满,AI 越容易"失忆"。

从"随手问 AI"到"把能力封装起来"

针对上述痛点,我们沿用了一条反复验证的结论:规范执行是人的短板、AI 的长板;业务判断是 AI 的短板、人的长板。Harness 工程的目标,就是把"执行层"的不稳定因素系统性地消掉:把规范写进 hooks,不再靠 AI 记忆,每次写 SQL 文件后自动触发检查;把迭代约束写进持久化文件,compact 后自动重新注入,不再靠临时口头说;把高 token 操作隔离到 subagent,主 context 只接收摘要,不被过程数据撑满。

二、先搞清楚"Harness"是什么

在 Claude Code 的 update-config skill 描述中有一句话:"Automated behaviors require hooks configured in settings.json — the harness executes these, not Claude"。

Harness = Claude Code 的宿主运行框架,即 Claude Code 客户端本身这个"工具链容器"。它:管理 context window 生命周期;在 LLM 推理循环之外确定性地执行 hooks;协调 subagents 的生命周期;不依赖模型判断,直接执行配置的自动化行为。区别总结:

三、核心问题:compact 到底丢掉什么?

每次数仓开发 context 接近满时,auto-compact 触发(默认 95% 时触发),会把整个对话历史替换为一份摘要,token 缩减到原来的约 12%。哪些内容 compact 后丢失:

数仓场景最痛的点:对话中说的"这次用 OVERWRITE 模式"、"先忽略 field_a 字段" → compact 后全忘;SKILL 文件读了一半,compact 后前几个 step 的内容没了 → Claude 重复询问;自测跑出来 50 行结果,加上血缘查询的几十行表结构 → context 很快膨胀到 compact 阈值。

四、五层防御体系(从简单到复杂)

第一层:写死进 CLAUDE.md(立即可用)

机制:项目根目录 .claude/CLAUDE.md 每次 compact 后从磁盘重新注入,是最可靠的持久化位置。将当前迭代的关键信息写入,格式建议:

# 当前迭代状态(每次迭代手动更新)## 正在开发- 表:db_a.dws_table_a
 版本:V1.0
 node_id:1000000001
 状态:ETL开发阶段(Step 3/8)

## 本次迭代约束- 禁止修改:dwd_table_b(已上线,只读)
 分区字段:partition_dt(格式 yyyyMMdd,不是 dt)
 amount 字段单位:千元(不是元)

## 数仓全局规范
 建表:分区字段必须是 partition_dt string
 禁止:SELECT *UPDATE/DELETEWHERE
 金额字段用 DECIMAL(20,4),不用 DOUBLE
 INSERT 必须带 PARTITION 子句

操作规则:进入新迭代时,更新"正在开发"和"本次迭代约束"两节;上线后清空"本次迭代约束";全局规范长期保留,控制在 100 行以内。

第二层:Auto Memory 自动积累(已在运行)

机制:Claude 自动将跨会话发现写入 ~/.claude/projects//memory/MEMORY.md,每次 compact 后重新注入。数仓场景下主动触发 Claude 写记忆的时机:"这张表的 amount 字段单位是千元,请记住";"field_a 在特定场景下会为空,请记住这个踩坑";"本次 V1.0 的关键变更是 field_b 逻辑调整,请记住"。Claude 会自动写入 MEMORY.md,下次会话或 compact 后自动恢复。

第三层:hooks 自动验证(核心防御,解决"忘了检查"的问题)

这是解决"每次写完 SQL 自动检查"的关键机制。

配置文件位置

数仓项目根目录/
└── .claude/
    ├── settings.json          ← hooks 在这里配置
    ├── CLAUDE.md              ← 数仓规范上下文
    └── hooks/
        ├── validate_sql.sh          ← SQL 规范自动检查
        ├── block_dangerous_ddl.sh   ← 危险 DDL 拦截
        └── inject_context.sh        ← compact 后重注入上下文

settings.json 完整配置

{"hooks": {"PostToolUse": [{"matcher": "Write|Edit","hooks": [{"type": "command","command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate_sql.sh","timeout": 60,"statusMessage": "检查 SQL 规范..."}]}],"PreToolUse": [{"matcher": "Bash","hooks": [{"type": "command","command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block_dangerous_ddl.sh"}]}],"SessionStart": [{"matcher": "compact","hooks": [{"type": "command","command": "cat \"$CLAUDE_PROJECT_DIR\"/.claude/context/dw_conventions.md","statusMessage": "重注入数仓规范..."}]}],"Stop": [{"hooks": [{"type": "prompt","prompt": "检查用户要求的所有任务是否都已完成。如果还有未完成项,返回提示但不要重新开始。检查 stop_hook_active 是否为 true,如是则直接 exit。","model": "claude-haiku-4-5-20251001"}]}]}}

SQL 规范自动检查脚本

.claude/hooks/validate_sql.sh:

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo"$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))" 2>/dev/null)

# 只处理 .sql 文件
[[ "$FILE_PATH" != *.sql ]] && exit 0
[[ -z "$FILE_PATH" ]] && exit 0

SQL=$(cat "$FILE_PATH" 2>/dev/null)
[[ -z "$SQL" ]] && exit 0

ERRORS=()

# 规范1:禁止 SELECT *echo "$SQL" | grep -iqE 'SELECT\s+\*' && ERRORS+=("CRITICAL: 发现 SELECT *,必须明确列名")

# 规范2:INSERT 必须带 PARTITIONif echo "$SQL" | grep -iqE 'INSERT\s+(INTO|OVERWRITE)'; thenecho "$SQL" | grep -iqE 'PARTITION\s*\(' || ERRORS+=("CRITICAL: INSERT 缺少 PARTITION 子句")
fi# 规范3:DOUBLE 类型金额echo "$SQL" | grep -iqE '\bDOUBLE\b' && ERRORS+=("WARNING: 金额字段建议用 DECIMAL(20,4),不用 DOUBLE")

# 规范4:UPDATE/DELETE 必须有 WHEREif echo "$SQL" | grep -iqE '\b(UPDATE|DELETE)\b'; thenecho "$SQL" | grep -iqE '\bWHERE\b' || ERRORS+=("CRITICAL: UPDATE/DELETE 缺少 WHERE 条件")
fiif [ ${#ERRORS[@]} -gt 0 ]; thenecho "=== SQL 规范检查失败:$FILE_PATH ===" >&2
  for err in"${ERRORS[@]}"; doecho "  $err" >&2
  doneexit 2
fiecho "SQL 规范检查通过: $(basename $FILE_PATH)" >&2
exit 0

危险 DDL 拦截脚本

.claude/hooks/block_dangerous_ddl.sh:

#!/bin/bash
INPUT=$(cat)
CMD=$(echo"$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))" 2>/dev/null)

# 拦截生产表 DROP/TRUNCATE(放行 _dev/_test/_stg 后缀)if echo "$CMD" | grep -iqE '\b(DROP\s+TABLE|TRUNCATE\s+TABLE)\b'; thenif ! echo "$CMD" | grep -qiE '(_dev|_test|_stg)\b'; thenecho "BLOCKED: 检测到生产表 DROP/TRUNCATE 操作,请确认表名是否正确" >&2
    exit 2
  fifiexit 0

hook 通信协议关键规则

图片

关键陷阱:阻断必须用 exit 2,用 exit 1 不会阻止 Claude 继续执行。

第四层:subagents 做上下文隔离(防 context 膨胀)

核心原则:把"高 token 消耗但结果只需要摘要"的操作放到 subagent 的独立 context 中执行。

数仓场景适合下放 subagent 的操作

图片

创建 subagent 文件

.claude/agents/sql-validator.md(SQL 语法验证):

---
name: sql-validator
description: ODPS/MaxCompute SQL 语法验证与规范检查专用 agent。当用户生成或修改 SQL 文件后需要验证时调用。在独立 context 运行,防止大量验证日志污染主对话。
tools: Read, Bash, Grep, Glob
model: haiku
permissionMode: dontAsk
---

你是数仓 SQL 规范专家,只做验证,不修改文件。

验证项(按优先级):
1. SELECT * 禁止
2. INSERT 必须带 PARTITION
3. 字段用 snake_case 命名
4. 金额字段用 DECIMAL 不用 DOUBLE
5. 多表 JOIN 必须有 ON 条件
6. 笛卡尔积风险检测

输出格式:
- 状态:PASS / FAIL
- 问题列表(CRITICAL / WARNING / INFO)
- 修改建议(具体到行号)

不超过 50 行,只返回结构化报告。

.claude/agents/dw-explorer.md(防止血缘查询撑爆主 context):


---
name: dw-explorer
description: 数仓结构探索 agent。当需要大量读取表结构、DDL、字段信息、血缘关系时自动调用,避免大量文件内容污染主 context。只读,不修改任何文件。
tools: Read, Glob, Grep, Bash
model: haiku
permissionMode: dontAsk
---

你是数仓探索专家,只读操作。

当被调用时:
1. 读取指定表的 DDL、字段信息、分区策略
2. 分析上下游血缘(一层)
3. 识别关键字段口径(金额、日期、状态类字段)

输出:不超过 80 行的结构化摘要,包含:
- 表基本信息(层级、粒度、分区策略)
- 核心字段定义(含口径说明)
- 上下游血缘(只列表名,不展开内容)
- 发现的特殊口径或踩坑点

使用方式

在主对话中自然触发:

用 dw-explorer 分析 db_a.dwd_table_a 的结构

对刚生成的 insert_dws_table_a.sql 用 sql-validator 验证

或强制指定(避免 Claude 自行判断):

@"sql-validator (agent)" 验证 path/to/insert.sql

第五层:SKILL 文件改造(减少 context 消耗)

当前的问题:每次调用 SKILL 文件(01~08.md),内容全部加载进主 context,加速 compact 触发。改造方向:把 SKILL 文件的"执行步骤"提炼成 subagent 指令,subagent 内部读完整 SKILL 文件,主 context 只接收结果摘要;用 path-scoped rules 替代 SKILL 文件中的规范章节,按需加载:

---
# .claude/rules/etl-rules.md
paths:
  - "**/*insert*.sql"
  - "**/*_di.sql"
  - "**/*_df.sql"
---

# ETL 开发规范(按文件路径自动加载)
- 必须有 partition_dt 分区
- INSERT OVERWRITE 前检查分区是否已存在
- 不允许跨库 JOIN

SKILL 文件保持现状,但改变调用方式:不要在主对话中直接触发,而是启动一个 subagent 来读 SKILL 文件执行 ETL,主对话只接收最终产出的 SQL 文件路径。

五、可行落地方案:数仓 Harness 架构

先让我们看一下整体的架构设计(整体架构图):

图片

这张架构图的核心逻辑是职责分层:不同类型的工作交给最合适的机制去做,而不是全部压在 Claude 的推理循环里。

持久化层解决的是"失忆"问题,任何临时口头说的约束,compact 后都会消失;但写进 .claude/CLAUDE.md 的内容,每次会话启动和 compact 后都会从磁盘重新注入——这是整套方案里最简单也最可靠的一层。

Harness 层(Hooks) 解决的是"规范靠记忆"的问题,PostToolUse hook 在每次写 .sql 文件后确定性触发,不依赖 Claude 有没有记住规范要求;违规时 exit 2 强制阻断,Claude 必须修正后才能继续,规范遵守率从 70%80% 提升到 95%+。

Subagent 层解决的是"context 被撑满"的问题,血缘查询、23 项自测、数据比对这类操作会产生大量 token,放到独立 context 的 subagent 里执行,主会话只接收一份摘要,compact 触发频率预计降低 50%70%。

三层机制分工明确,互不干扰:Hooks 处理"写动作触发的规范检查",subagent 处理"读操作的 context 隔离",CLAUDE.md 处理"跨会话状态持久化"。为了配合这套架构落地,数仓的研发流程可以拆解为 8 个大的步骤,流程图如下:

图片

从 Harness 架构的视角来看,8 个步骤可以按"对 context 的影响"分成两类:一类是直接在主会话处理(内容量有限,context 压力低):需求分析(读 PRD)、技术设计(写规范说明)、SR 导入(生成配置)、SLA/DQC(生成规则)。另一类必须通过 Harness 机制处理(否则会加速 compact 或规范失控):ETL 开发每次写 .sql 文件,PostToolUse hook 自动触发规范检查,不依赖人工提醒;自测时 23 条 SQL 的执行结果体量大,交给 data-quality-checker subagent 隔离,主会话只收 PASS/FAIL 摘要;数据比对时两表样本数据量大,交给 data-comparator subagent 隔离;性能优化时血缘 + 多层 DDL 每次 500~3000 tokens,交给 dw-explorer subagent 隔离。这种分工不是把步骤拆开独立执行,而是在同一个工作流里,让每个步骤以最合适的方式运行——context 压力小的步骤留在主会话保持流畅,context 压力大的步骤通过 subagent 隔离保持干净,规范检查通过 hook 自动执行不需要人工干预。

六、基于 SKILL 规范的数仓工作流设计

数仓 8 步 SKILL 规范(需求分析→技术设计→ETL→自测→数据比对→SR 导入→性能优化→SLA/DQC)天然对应 Harness 的三层机制。核心思路是:SKILL 文件不变,改变调用方式——主对话只读规范结论,实际执行由 subagent 或 hook 完成。

图片

各步骤推荐提示词与工作流

Step 1:需求分析

推荐提示词:

用 dw-explorer subagent 先读取上游表结构(只返回摘要),
然后按需求分析规范生成:
1. 需求摘要(≤5行)
2. 表字段口径草稿
3. 待确认问题清单(按优先级排序)
需求文档 URL:[粘贴PRD链接]

Hook 配合:SessionStart 注入当前迭代约束(版本号/表名/禁止修改的表)。

Step 2:技术设计

推荐提示词:

基于上一步确认的需求,按 OneData 规范完成技术设计:
 表名:[按 层级_域_主题_粒度_周期 格式命名]
 粒度:[描述]
 分区:partition_dt string(格式 yyyyMMdd)
 禁止:任何与上游不一致的字段命名
输出 OneData 建模说明,不超过 60

CLAUDE.md 写入(设计完成后手动更新):

## 当前迭代技术设计决策- 表名:db_a.dws_table_a
- 主键:order_no + partition_dt
- 特殊约束:amount 字段继承上游千元单位,不做转换

Step 3:ETL 开发

这是 Harness 工程价值最高的步骤,PostToolUse hook 在每次 SQL 文件保存时自动触发。

推荐提示词:

按 ETL 开发规范生成建表 DDL + Insert SQL- 建表文件:ddl_[表名].sql
- 插入文件:insert_[表名].sql
- 要求:INSERT 用 OVERWRITE 模式,PARTITION 子句必须包含 partition_dt
- 金额字段:DECIMAL(20,4),单位继承上游(千元)
生成完毕后,用 sql-validator subagent 验证两个文件

Hook 自动执行(无需手动触发):每次写入 .sql 文件 → PostToolUse hook 自动运行规范检查。若发现 SELECT * 或缺少 PARTITION → 返回 exit 2,Claude 收到错误自动修正。

Step 4:自测

推荐提示词(防止自测结果撑爆主 context):

用 data-quality-checker subagent 对 [表名] 执行 23 项标准自测,
bizdate = [日期]
补充口径约束:[如"is_perform=1 只取履约订单"]
只返回:PASS/FAIL 汇总 + FAIL 项详情(≤50行),不返回原始 SQL 执行结果

效果:23 条 SQL 的执行结果全在 subagent context 里,主对话只收到一份摘要报告。

Step 5:数据比对

推荐提示词:

用 data-comparator subagent 对比:
 新表:[新表名] partition_dt = [日期]
 参考表:[旧表名/线上表]
 比对字段:[核心金额字段列表]
 容差:≤ 0.01%(金额类)
只返回:差异超过容差的字段列表 + 差值,不返回全量对比数据

Step 6:SR数据库导入

推荐提示词(自动生成最优数据库建表参数):

用 dw-sr SKILL生成建表任务, 先查以下表的 DDL 和一层上下游血缘(只返回摘要):
- 源表:[ODPS表名]
- 目标表:[SR表名]

然后基于 DDL 摘要,分析当前 SR 同步任务的配置风险:
1. 字段类型是否有精度丢失风险(DECIMAL/DOUBLE → DECIMAL(38,18))
2. Key 字段选择是否合理(重复率是否过高导致 DUPLICATE KEY 膨胀)
3. 分区数量是否合理(partition_live_number 与下游查询窗口是否匹配)
4. DISTRIBUTED BY HASH 的 bucket 数与数据量是否匹配
5. 是否有 DATETIME 字段在 SR 侧用了 VARCHAR 存储(会导致时间过滤走全表扫描)

输出同步任务配置建议(按风险高低排序),不超过 20 行。
每条格式:[风险等级 高/中/低] 问题描述 → 建议修改方式

Step 7:性能优化

推荐提示词(防止血缘查询撑爆主 context):

用 dw-explorer subagent 先查 [表名] 的一层上下游血缘和 DDL(只返回摘要),
然后分析当前 Insert SQL 的性能瓶颈:
1. 是否有全表扫描
2. 是否有笛卡尔积风险
3. 是否可以用 MAP JOIN 替代 HASH JOIN
输出优化建议(按收益排序),不超过 30

Step 8:SLA/DQC

推荐提示词:

按 SLA/DQC 规范为 [表名] 生成 9 类 DQC 规则:
 完整性:主键非空、分区数据量
 准确性:核心金额字段与上游比对(容差 0.01%)
 一致性:is_perform 与 perform_flag 联动逻辑
 时效性:产出时间 SLA ≤ 次日 8:00
输出 DQC 配置 JSON,可直接使用

SKILL 调用方式改造(减少主 context 消耗)

当前问题:触发 SKILL 时,SKILL 文件全文(约 10KB)加载进主 context,加速 compact。改造方向:

图片

核心原则:主对话只接收决策级信息(PASS/FAIL、差异字段、优化方案),不接收过程数据(SQL 执行结果、原始血缘列表、完整 DDL 内容)。

精准对话流设计:控制AI思考的艺术

文章中多次提到 /dw-etl、/dw-自测 等命令,这是数仓研发全流程 SKILL 套件的核心触发词,每个命令对应一个封装了规范、产出格式和工具调用的标准化执行单元。核心理念:把每次都要重复讲的要求写进 SKILL,把每次都怕忘的检查点写进 SKILL,把每次都需要的结构化输出也写进 SKILL——这样谁来做需求都行,底座是一致的。

8 个 SKILL 命令一览

图片

以 /dw-etl 为例:一条命令封装了什么?ETL 开发(Step 3)是 Harness 工程价值最高的步骤,SKILL 文件内封装了:① 规范内容(写死,不靠记忆):建表规范:分区字段必须是 partition_dt STRING(格式 YYYYMMDD);金额字段:DECIMAL(26,4),不用 DOUBLE;INSERT 模式:必须使用 INSERT OVERWRITE,必须带 PARTITION 子句;禁止:SELECT *、跨库 JOIN(非血缘关系)。② 产出格式(结构化,不走样):

输出文件:
├── ddl_[表名].sql      ← ODPS 建表语句(含注释三段式、生命周期配置)
├── insert_[表名].sql   ← ODPS Insert SQL(含分区裁剪、JOIN 规范)
└── ddl_sr_[表名].sql   ← SR 建表语句(Key 列顺序、DECIMAL 精度)

③ 自动护栏(Hook 配合):每次 .sql 文件写入后,PostToolUse hook 自动执行 validate_sql.sh;发现 SELECT * 或缺少 PARTITION → exit 2 阻断,Claude 强制修正。④ subagent 卸载(防 context 膨胀):"生成完毕后,用 sql-validator subagent 验证两个文件";验证结果全在 subagent context 里,主对话只收到"PASS/FAIL + 问题列表"。综合效益数据:

图片

七、落地步骤

步骤一:项目级上下文持久化。在数仓项目目录下创建 .claude/CLAUDE.md,写入当前迭代状态。每次新迭代开始时更新"正在开发"和"本次迭代约束"两节,上线后清空约束节。全局规范永久保留,控制在 100 行以内。

步骤二:配置 hooks 自动验证。创建 .claude/settings.json + hooks/ 目录,配置 PostToolUse hook(每次写 .sql 后自动规范检查)和 PreToolUse hook(拦截危险 DDL)。效果:不需要每次手动说"帮我检查 SQL 规范",hook 在 Harness 层自动触发,不占用 Claude 推理资源。

步骤三:创建 subagents 隔离高 token 操作。创建三个核心 subagent 文件:sql-validator.md、dw-explorer.md、data-quality-checker.md。将 Step 4/5/7 的执行全部下放,预计主 context compact 频率降低 50%~70%。

八、Harness 工程能解决的核心问题

这一节是整个方案的出发点,也是对"为什么要这么做"的直接回答。

数仓 AI 开发当前的本质瓶颈:语义理解

在实际数仓 AI 开发中,技术能力不是瓶颈,语义理解才是。需求理解偏差占总返工的 40%~60%,大约 40%~50% 的工作时间花在理解需求和与业务核对口径上。精准血缘探查:准确率提升显著,远超传统方式。这两个观察揭示了同一个问题:AI 在数仓场景的不准确,根本原因不是"不会写 SQL",而是"不理解业务语义"——不知道这张表的 amount 单位是千元还是元,不知道 is_perform=1 在这个版本里意味着什么,不知道某字段在特定场景下会为空。

语义 × 数据 = 准确率

数仓 AI 开发的准确率,可以用一个公式来理解:

准确率 = 语义理解深度 × 数据规范覆盖度

图片

结论:Harness 工程的本质,是把"语义"和"规范"从不可靠的 LLM 记忆中,迁移到确定性的 hooks + 持久化文件里,从而让 语义 × 规范 = 准确率 这个等式两边的变量都变得稳定。

具体能解决的四类问题

问题一:字段口径遗忘导致的计算错误

现状:对话开始时告知了"amount 字段单位是千元",compact 后 Claude 忘了,生成的 SQL 把千元当元用,导致数据差 1000 倍。Harness 解法:字段口径写进 .claude/CLAUDE.md(compact 后重新注入);踩坑经验写进 Auto Memory(跨会话持久化);结果:这类错误从"时常发生"降到"基本不出现"。

问题二:需求理解偏差导致的返工

现状:需求文档描述的是"用户视角的 GMV",但 AI 生成了"交易视角的 GMV",两者口径不同,数据对不上,需要返工。Harness 解法:需求分析的产出(口径草稿 + 待确认问题清单)写进 CLAUDE.md 的迭代约束节;Stop hook 检查任务完整性,确认待确认问题清单已全部明确后才放行;结果:需求一次交付通过率从约 50% 提升到 90%。

问题三:SQL 规范执行不一致

现状:规范文档写了"INSERT 必须带 PARTITION",但 Claude 有时会忘,或在 compact 后规范内容被清除后生成不合规的 SQL。Harness 解法:PostToolUse hook 在每次写 .sql 文件后自动执行规范检查,不依赖 Claude 记忆;违规时 exit 2 返回错误,Claude 强制修正后才能继续;结果:规范执行率从"依赖 LLM 记忆的 70%~80%"提升到"hook 强制的 95%+"。

问题四:大型需求开发中的 context 耗尽

现状:复杂需求(大型宽表、大量下游血缘)开发过程中,血缘查询 + 自测结果 + SKILL 文件内容堆满 context,compact 触发后丢失关键约束,Claude 开始犯低级错误。Harness 解法:血缘查询 → dw-explorer subagent(独立 context,只返回摘要);23 项自测 → data-quality-checker subagent(独立 context,只返回 PASS/FAIL 报告);SKILL 文件内容 → subagent 内部读取,主 context 只接收产出的 SQL 文件路径;结果:主 context compact 频率预计降低 50%~70%。

与传统数仓 AI 开发方式的对比

图片

从"AI 帮我写代码"到"AI 嵌入研发流水线"

Harness 工程的最终目标,不是让 Claude 更聪明,而是让整个研发流水线更可靠。Claude(LLM)负责:理解需求、设计方案、生成代码——这些需要语义理解的事;Harness(hooks)负责:规范检查、危险拦截、任务完整性验证——这些需要确定性执行的事;Subagents 负责:大量文件读取、血缘查询、自测执行——这些会消耗大量 context 的事;CLAUDE.md + Memory 负责:字段口径、迭代约束、踩坑经验——这些需要跨会话持久化的事。这四层分工,让数仓 AI 开发从"对话驱动的一次性辅助"升级为"规则嵌入的流水线自动化"。

往期回顾

1.BP Claw 破解 AI 编码输入难题 ——FlinkSpec 需求智能化实践|得物技术

2.基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术

3.通用 AI Agent 驱动网关路由安全审计实践|得物技术

4.AI驱动:从运营行为到自动化用例的智能化实践|得物技术

5.生成式召回在得物的落地技术分享与思考

文 /丹克

关注得物技术,每周三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

BP Claw 破解 AI 编码输入难题 ——FlinkSpec 需求智能化实践|得物技术

作者 得物技术
2026年5月14日 13:30

一句话理解 BP Claw:它是 FlinkSpec 上游的 “AI 数据 BP”,把产品经理的非标 PRD 自动转化为 AI Coding 可消费的高质量需求文档。在深入阅读之前,先用一张表告诉你 BP Claw 能带来什么。

image.png

如果你的团队也在被以下问题困扰,这篇文章值得读完:AI Coding 效果不稳定,同样的框架、不同的需求,结果天差地别;需求沟通来回反复,一个指标口径要确认三四轮;PRD 质量参差不齐,开发阶段频繁踩雷、返工延期。BP Claw 的答案是:问题的根源在输入,解法在源头。

一、FlinkSpec:实时数仓的 AI 工程化底座

在深入 BP Claw 之前,我们先来看一个更大的图景 ——FlinkSpec。FlinkSpec 是实时数仓团队打造的 AI 工程化开发框架,它以 “需求驱动、制品为锚” 的理念,将一条实时数据链路的生命周期拆解为四个阶段,每个阶段都有严格的门控评估,确保交付质量。

FlinkSpec 全景架构

image.png

BP Claw 在 FlinkSpec 中的定位

在 FlinkSpec 的四个阶段中,需求澄清阶段是一切的起点。而需求澄清阶段的质量,取决于一个关键输入 ——PRD 文档。如果 PRD 质量不达标,后续的一切都是空中楼阁:需求定义模糊 → Intake 门控反复打回 → Define 阶段耗时翻倍;技术口径缺失 → Flink-SQL Agent 无法编码 → 频繁触发阻塞协议(BLOCK-N);验收标准不清 → Review 阶段发现问题 → 回退到 Define 重来。

image.png

BP Claw 解决实际问题案例

问题定义

商业化广告投放链路中,一次广告曝光往往由多个商家共同参与竞价。这种一对多的流量-主体映射关系,在指标聚合时会产生一个结构性的歧义:以"商家"为维度下钻时,同一次流量事件会被多个商家各自计入,导致商家侧明细加总后,与平台侧总量不等。这不是数据质量问题,而是两套在各自语义下都正确的口径共存在同一张宽表里,却没有被显式区分:impression_cnt(平台侧)去重到流量维度,一次曝光 = 1;impression_cnt(商家侧)展开到主体维度,N 个商家参与 = N。当下游消费方使用同一个字段名、却按不同语义聚合时,CTR、CVR、ROI 等衍生指标就会系统性失真。

为什么难以在开发阶段发现

实时数仓的指标开发通常以 FlinkSQL 为载体,开发侧看到的输入是 Kafka 事件流和维度表,字段语义的边界在建模时已经隐式确定。问题在于: FlinkSQL 层面的 GROUP BY 逻辑无法自动感知上游指标应该以哪个粒度聚合。如果流量事件里同时携带了 advertiser_id 列表,不同的展开方式对应完全不同的业务语义,而这一选择在代码里只是一行 UNNEST 或一个 JOIN 的差异。常规的 Code Review 能发现 SQL 逻辑错误,却很难发现"SQL 逻辑正确、但口径选择与业务预期不符"这类问题——因为两种写法都能跑通,数据差异在没有对比基准的情况下也不会报错。

BP Claw 的介入点

BP Claw 作为需求到编码之间的中间层,其核心能力是在指标定义阶段做口径显式化,而不是在开发完成后做数据校验。

具体做法是结合 OpenViking 知识库(团队历史建模文档、字段定义、历史踩坑记录)做语义召回,识别当前需求涉及的指标是否存在多粒度歧义,并在进入 FlinkSpec 之前,将以下约束显式写入需求文档:

image.png

这套约束作为 FlinkSpec 的输入,让 AI Coding 在生成 DDL 和 INSERT 逻辑时,直接基于已经对齐的语义边界,而不是从模糊描述中自行推断。

与通用 AI Coding 的差异

通用 AI Coding 工具在给定输入后,会选择一种"看起来合理"的实现——对于多商家指标,它会倾向于展开商家维度(因为需求里写了"按商家查看"),但不会主动判断这是否会导致平台侧口径膨胀。

BP Claw 的差异在于知识来源:它使用的不是通用语言模型对业务逻辑的推断,而是团队在同类建模问题上积累的历史决策和约束条件。对于"多商家指标是否需要分口径建模"这个问题,OpenViking 里已经有过明确结论,BP Claw 将其召回并作为强约束注入当前需求,而不是重新推断一次。

BP Claw 正是为了解决这个"源头问题"而生。它站在 FlinkSpec 的上游,确保进入 Define 阶段的 PRD 文档质量足够支撑后续的 AI 自动化编码。

二、产品设计:从痛点出发的解决方案

面向谁?解决什么?

image.png

设计哲学:贴合工作流,而非改造工作流

BP Claw 的设计遵循三条核心原则,原则一:嵌入现有场景——用户不需要打开新页面、学习新工具,直接在飞书群聊中 @ 机器人即可触发,和日常"拉群评审"的行为完全一致;原则二:产品经理零感知提效——产品经理只需要提交原始 PRD 文档,BP Claw 自动完成规范化转换,产品经理无需关心格式,产品经理收到的是结构化、高质量的评审群,而非一堆格式要求;原则三:不硬卡点,做参照系——PRD 评分是"参照",不是"阻塞",帮助团队建立质量意识,而非制造流程摩擦,逐步提升 PRD 质量基线,而非一刀切。

三、核心能力层

对实时数仓而言,一份好的 PRD 应该是什么样的?

在深入技术实现之前,我们先回答一个根本性问题:实时数仓对 PRD 文档的要求是什么?一份好的实时数仓 PRD,需要覆盖七大模块:

image.png

然而,产品经理提交的文档往往只覆盖了其中的 2-3 个模块,而且格式五花八门。这正是 BP Claw 要解决的核心问题 —— 需求转化。

能力一:智能需求转化(核心能力)

这是 BP Claw 最核心、最有技术含量的能力。它做的事情,本质上是 模拟一个经验丰富的数据 BP 的工作过程。

转化过程详解

image.png

转化的技术难点

如何处理常见的业务需求类型?我们总结了实时数仓中最常见的几类需求模式:

image.png

转化的核心原则:忠实于原文

原文有的信息进行结构化提取,按模板填充;原文没有的信息标记为待补充,绝不凭空捏造;原文有歧义的信息保留原始表述,同时标注歧义点;原文划删除线的标注为"本期不纳入,暂缓"。

能力二:PRD 质量评分(参照能力)

在完成需求转化后,BP Claw 会对标准化文档进行多维度质量评估,生成诊断报告。

五大评分维度

image.png

三条核心评分规则

规则一:技术口径一票否决——如果技术口径得分 < 15 分(满分 25),即使总分 ≥ 90,也标注为不可执行。原因:没有技术口径,FlinkSpec 的 Flink-SQL Agent 完全无法启动编码。规则二:验收标准缺失扣分——验收标准完全缺失,总分直接扣除 20 分;严重不足(≤5 分),扣除 10 分。原因:没有验收标准的需求,上线即翻车。规则三:溢出得分机制——某维度特别优秀可超出该维度满分,但总分上限仍为 100 分。原因:鼓励在关键维度上做到极致。

image.png

需要强调的是:PRD 评分不是硬卡点,不是流程阻塞。它是一面镜子,帮助团队看到当前 PRD 的质量水位,逐步建立质量意识。

能力三:自动拉群(工作流融合能力)

自动拉群看似简单,但它承载着一个重要的产品设计意图——降低落地阻碍。为什么做自动拉群?如果 BP Claw 只能生成文档,用户还需要手动拉群、手动 @ 人、手动转发文档,那使用体验就是割裂的。自动拉群让整个流程变成了"一步触发、全程自动":用户只需要在飞书群里 @ 机器人、@ 评审人,附上文档链接。BP Claw 自动完成:读取文档 → 生成标准化文档 → 质量评估 → 创建评审群 → 邀请成员 → 发送文档和诊断报告。评审人在新群里直接看到结构化的文档和质量报告,开始评审。这就是"贴合现有工作流"的具体体现——用户的行为没有变,但背后的效率翻了20倍。

四、技术难点与解决方案

省 Token 的技巧

在 AI 应用中,Token 消耗直接关系到成本和响应速度。BP Claw 在这方面做了多项优化。

技巧一:分段生成策略

对于包含 10 个以上指标的大文档,我们不会一次性生成完整文档(这会导致上下文溢出),而是采用分段生成:先创建文档骨架(业务背景 + 角色-页面-指标矩阵),逐个追加指标定义模块,每追加 5 个指标发送一次进度反馈,最后追加验收标准和用数链路。本质是把"一次大事务"拆成"多次小提交",即使中途失败也保留了已写入内容,不需要从头来。同时每段写入后向群聊发送进度消息(已生成 5/12 个指标),让用户有等待感知,不会误以为机器人卡死。收益:避免单次 API 调用超时,同时减少重复上下文传递的 Token 消耗。

技巧二:分层调用架构

BP Claw 采用编排者(动态调度,按需组合 Skill,职责清晰)模式,将核心逻辑拆分到三个独立 Skill 中:

image.png

每个 Skill 独立运行、独立管理上下文,避免了将所有逻辑放在同一个上下文中导致的 Token 膨胀。

技巧三:模板化 Prompt 设计

标准化文档的七大模块,每个模块都有严格定义的模板结构。通过模板约束 AI 的输出格式,避免了冗余输出:明确指定输出字段,不多不少;使用 Markdown 表格约束结构;通过 Few-Shot 示例引导输出风格。

稳定性保障:如何避免幻觉?

这是 BP Claw 最重要的技术挑战。AI 生成的文档如果出现"幻觉"(编造不存在的业务口径),后果比不生成还严重。

策略一:严格的忠实性约束

在 Skill 的核心 Prompt 中,我们设置了多层约束:

核心规则:忠实于源材料原文有的 → 结构化提取原文没有的 → 标记 ⚠️ 待补充绝不凭空发明业务定义绝不猜测技术口径

策略二:标记机制取代猜测

当 AI 遇到无法确定的信息时,不允许"猜测后继续",而是必须:明确标记为待补充;说明缺失的具体内容;给出建议补充的方向。这样做的好处是:用户看到的文档中,每一条确定的信息都是可信的。

策略三:质量评分的交叉验证

prd-quality-scorer 作为独立的评分 Skill,会对生成的文档进行"第二遍"审查。如果文档中存在明显的逻辑矛盾或不合理之处,评分报告中会直接指出。

策略四:参数校验与重试机制

• 调用飞书 API 前,强制校验所有必填参数• 文档生成失败 → 最多重试 3 次(间隔 2s → 5s → 10s 指数退避)• 3 次均失败 → 终止流程,发送明确的错误消息• 绝不降级生成"简化版"文档(不完整不如不生成)

打磨 Skill 的技巧与难点

难点一:如何让 AI 理解"实时数仓"的领域语义?普通的 LLM 并不理解"去重视图""Lookup Join""sink.properties.columns"这些领域概念。我们通过以下方式解决:领域知识注入(在 Skill 定义中嵌入实时数仓的核心概念解释);模板驱动(通过模板结构限定输出范围,减少 AI 的"自由发挥空间");Few-Shot 示例(提供真实的标准化 PRD 样例作为参考)。

难点二:如何处理超长文档?有些 PRD 文档超过 10000 字符、包含 15+ 指标,直接处理会导致 API 超时(默认 60 秒不够用)、Token 超限、输出截断。解决方案:显式设置 timeoutSeconds=1200(20 分钟);采用分段生成策略,每次只处理 3-5 个指标;实时进度反馈,让用户知道系统仍在工作。

难点三:如何协调多个 Skill 的执行顺序?BP Claw 作为"指挥家/编排者",需要严格控制 4 个步骤的执行顺序和依赖关系:

规则:严格串行,前序成功才执行后续STEP 1 成功 → STEP 2STEP 2 成功 → STEP 3STEP 3 成功或失败 → STEP 4(评分失败不阻塞)STEP 4 成功 → 完成任一关键步骤失败 → 立即终止,通知用户

为什么 STEP 3 失败不阻塞?因为质量评分是"参照"而非"卡点"。即使评分失败,标准化文档和评审群依然有价值。

五、与 FlinkSpec 的联动:全链路赋能

BP Claw → FlinkSpec 的价值传递

image.png

体验效果:PRD 质量提升对 AI Coding 的赋能

场景一:技术口径完整的 PRD——BP Claw 评分 ≥ 90 分,FlinkSpec Define 阶段一次通过,无 BLOCK,Flink-SQL Agent 直接编码无需人工干预,整体交付周期为天级 ⏱️。场景二:技术口径缺失的 PRD——BP Claw 评分 < 60 分,触发技术口径一票否决,FlinkSpec Define 阶段频繁触发阻塞协议,Flink-SQL Agent 无法编码需反复沟通,整体交付周期为周级。结论: BP Claw 每提升 10 分的 PRD 质量,FlinkSpec 的编码阶段效率约提升 30%。

六、落地运营:产品 + 运营 = 真正的落地

一个好的产品工具,如果没有运营手段的配合,是无法真正落地的。BP Claw 在落地过程中,我们采用了产品能力 + 运营能力双轮驱动的策略。

运营手段一:成熟度评分体系

我们建立了 域级 PRD 成熟度评分 机制,通过持续追踪各业务域的 PRD 质量水位,推动整体提升:

image.png

运营手段二:质量趋势追踪

通过持续收集每次 PRD 评分数据,建立质量趋势看板:按业务域维度追踪质量变化;识别高频缺失项,定向改进;月度质量复盘,表彰优秀案例。

运营手段三:最佳实践沉淀

将高分 PRD 案例沉淀为模板,形成可复用的知识资产:优秀案例库(≥ 90 分的 PRD 自动入库);改进指南(针对常见扣分项提供标准化的补充模板);新人培训(通过 BP Claw 评分报告快速上手 PRD 编写规范)。

七、快速上手

使用方法

只需一步:在飞书群聊中发送一条消息:

@商业化MOSS @评审人1 @评审人2 @评审人N.... 需求拉群 飞书PRD文档链接

image.png

然后等待 1-5 分钟,你将在新的评审群中看到:标准化 PRD 文档、质量诊断报告、改进建议清单。

注意事项

image.png

八、展望后续

本文为 FlinkSpec 系列之开篇,亦是这场工程化变革的序章。BP Claw 所立之处,不过链路之源。FlinkSpec 所图,乃以 AI 之力,将实时数仓从需求落地至验收上线的全程工序,熔铸为一套精密自洽、生生不息的智能工程体系。宏图未竟,后续系列将逐章揭幕,敬请期待!

往期回顾

1.基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术

2.通用 AI Agent 驱动网关路由安全审计实践|得物技术

3.AI驱动:从运营行为到自动化用例的智能化实践|得物技术

4.生成式召回在得物的落地技术分享与思考

5.立正请站好:一个组件复用 Skill 的工程化实践|得物技术

文 /子宸

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术

作者 得物技术
2026年5月7日 11:12

一、核心理念:Harness 思维 — 让 AI 模仿,而不是凭空创造

全栈 AI 开发最容易踩的坑

全栈 SDD 开发中,最常见也最致命的错误是:让 AI 从零开始写代码。AI 模型具备"通识能力",给它一个需求描述,它确实能生成可运行的代码。但问题在于,这些代码往往是"外星代码":风格不一致(命名规范、目录结构、分层方式与项目现有代码不同)、复用率低(没有利用项目已有的公共组件、工具函数、请求封装)、采纳率低(Code Review 时后端同学看到"外来风格"的代码,会产生大量修改意见)。结果就是:AI 生成了代码,但 Review 成本和返工成本反而更高了。

Harness 思维的核心:给 AI 一个"模仿对象"

Harness(约束)思维的本质是:给 AI 一个已有的实现作为参照,让它照着复刻一份,而不是凭空创造。就像给一个新入职的工程师说"你照着这个模块的风格,写一个类似的",而不是"你自由发挥"——前者往往能更快产出符合团队规范的代码。

image.png

在提示词中体现 Harness

不推荐(凭空创造):

请实现一个结束语管理的 CRUD 接口

推荐(Harness 约束):

请参照现有"场景欢迎语"功能(后端接口 /api/v1/feature/list,前端入口 FeatureTable/index.tsx:53-58)实现"结束语"功能。数据结构、分层方式、命名风格都保持一致。新增场景 code:categoryCode = "SCENARIO_CLOSING"

两者的差距不在于 AI 是否"聪明",而在于你给了 AI 多少约束和上下文。约束越精准,生成代码的可用性越高。

二、全栈工作区搭建与 Codebase Indexing

为什么要搭多仓工作区?

前后端代码通常分布在两个独立仓库。如果分开打开,AI 生成后端接口时看不到前端的调用方式,生成前端代码时看不到后端的返回结构,接口字段对不上是家常便饭。将前后端代码放在同一个工作区下,有三个核心价值,Codebase Indexing:Cursor 对工作区内所有代码进行向量化嵌入,建立语义索引。AI 能跨仓库理解代码关系,生成质量大幅提升。上下文完整:AI 同时能看到前后端代码,接口字段、命名风格自然对齐。SDD 文档集中管理:前后端 SDD 文档在同一工作区,便于接口契约对齐。

Codebase Indexing 的价值

Cursor 的 Codebase Indexing 会对工作区内的代码进行向量化嵌入,建立语义索引。这意味着:当你问 AI "场景欢迎语是怎么实现的",它不需要你手动指定文件,能通过语义检索自动找到相关的 Controller、Service、前端组件。当你让 AI "照着欢迎语写结束语",它会检索到欢迎语的前后端完整实现链路,而不只是单个文件。

前后端放在同一个工作区,Codebase Indexing 覆盖两侧代码。AI 生成后端接口时能参考前端的调用方式,生成前端代码时能参考后端的返回结构。

Tips:Cursor 打开工作区后,首次索引可能需要几分钟。可以在 Cursor 设置查看索引进度。确保索引完成后再开始让 AI 生成代码,效果会明显更好。

Cursor vs Claude Code:选哪个?

在全栈 AI 开发场景下,两款工具各有侧重,下表是实测对比:

image.png

全栈工作区搭建&SDD初始化--内部全栈研发插件

以上述需求为例,工作区结构如下,.claude 和 .cursor 中已对 SDD 能力进行初始化。

三、SDD 驱动的全栈代码生成流程

全栈 SDD 的特殊之处

与纯前端/纯后端 SDD 不同,全栈 SDD 需要:生成两份 SDD 文档(前端一份、后端一份);接口契约对齐,前端 SDD 中的接口调用与后端 SDD 中的接口定义必须严格对应;字段映射一致,前端 VO 中的字段名与后端返回的 JSON 字段名一一对应。

相关概念术语

提示词编写范式

以下是经过实践验证的全栈 SDD 生成提示词模板:

这是一个前后端全栈开发工作区,需要你设计技术接口方案,同时开发前后端项目;首先你需要 cd 到对应前后端应用目录中,创建 sdd 文件;所以你需要生成两份 sdd 文档,之后我会启动两个 agent 分别实现;在生成之前,如果你需要确认某些细节,你应当先确认后生成 sdd 文档。前端应用:service-frontend/sdd-propose  feature/your-feature-name前端修改入口参考:@FeatureTable/index.tsx:53-58 @columns/index.tsx后端应用:service-backend/sdd-propose  feature/your-feature-name后端修改入口参考接口:/api/v1/feature/list需求内容:(附上需求文档或描述,并提供前后端需求点清单)

关键要素解读:

image.png

前后端需求点清单分工示例

前端需求功能点

主要是新增一个后台管理页面的 tab,涉及到搜索、展示、配置新增、删除等;利用内部 SDD 文档工具(如下图)从 PRD 描述和文档图片中提炼出需求点。

内部 SDD 文档工具

左侧导航新增"结束语" Tab。 右侧新增结束语列表页。 字段有:结束语内容、结束语描述、优先级、更新人、更新时间、操作列。

新增 / 编辑弹窗字段:结束语描述、生效日期、生效时段、生效时段、结束语话术(类型和规则这里不一一罗列)。

拖拽排序功能: 点击"排序"按钮进入排序状态,拖拽调整顺序后点击"保存"生效。

后端功能点(含接口清单)

后端功能由 AI 根据前端需求描述自主设计数据表和接口。以下是 AI 在生成 SDD 前需要明确的关键设计问题(这些问题应在提示词中列出,让 AI 先回答再生成)。

接口清单: 列表接口(支持分页,回显数据直接嵌入列表响应,无需单独回显接口)、新增接口、编辑接口(复用新增逻辑,根据 id 更新)、删除接口(逻辑删除,修改删除状态字段)、排序接口(批量更新,需考虑高效实现方案)。

字段设计: 结束语话术内容(数组类型)、结束语描述(文本)、优先级 / 序号(小整型)、更新人(字符串)、更新时间(时间戳)。

需要 AI 在 SDD 中明确回答的设计问题:

  • 主键设计:如何设计主键字段?前端发起编辑、删除时需要传递该字段。
  • 优先级自增逻辑:优先级应基于当前数据条数自增,无需前端传递,由后端自动处理。
  • 排序如何高效更新:批量排序时如何设计接口,避免 N 次单条更新?
  • 嵌套对象如何建表:参考已有的"场景欢迎语"接口,入参中存在嵌套子对象(如下方参考结构)。此类子对象应拆分到多张表,还是序列化为 JSON 字段存单张表?
  • isNextDay 字段含义:次日逻辑的具体含义是什么?前端时段选择器中"次日"勾选状态如何映射到该字段?
  • 列表回显设计:列表接口需要返回完整的回显数据(供编辑弹窗回填),无需单独提供详情接口。

为什么要把这份清单放进提示词?

这份清单做了两件重要的事:前端侧给 AI 完整的 UI 细节,让 AI 知道组件状态、字段约束、交互逻辑,避免它做"最简实现"。后端侧把模糊的设计问题提前暴露,让 AI 在写 SDD 之前先回答这些问题——这正是 Harness 思维的体现:让 AI 参照已有实现(如"欢迎语")来解决"结束语",而不是凭空设计。

SDD 文档产出

一次完整的全栈 SDD 生成,会产出以下文档:

前端 SDD:

  • proposal.md — 需求提案,描述前端要做什么。
  • spec.md — 技术规格,组件设计、接口调用、状态管理。
  • tasks.md — 任务拆分,每个 task 对应一个可执行的代码变更。

后端 SDD:

  • proposal.md — 需求提案,描述后端要做什么。
  • spec.md — 技术规格,接口设计、数据库设计、分层架构。
  • design.md — 详细设计,类图、字段映射、SQL。
  • tasks.md — 任务拆分。

SDD 指令使用说明

典型工作流示例

入门引导:

  1. openspec-onboard(首次,不熟悉才走,引导完整步骤);
  2. openspec-continue-change(提示你下一步要干嘛);
  3. openspec-ff-change(快进)。

场景 A:初次开发

  1. openspec-explore(调研,脑暴);
  2. openspec-propose "..."(生成设计);
  3. openspec-apply-change(写代码);
  4. openspec-verify-change(自测,校验代码与 SDD 文档是否对应);
  5. openspec-archive-change(收尾,归档)。

场景 B:二次开发,修改迭代已有功能

  1. openspec-explore(定位旧代码/旧 spec);
  2. openspec-propose "修改..."(生成变更 Spec);
  3. openspec-apply-change(应用修改);
  4. openspec-verify-change(验证回归);
  5. openspec-archive-change(归档)。

场景 C:二次修改,需求变更

  1. openspec-explore(调研,脑暴);
  2. openspec-propose "..."(生成设计);
  3. openspec-apply-change(写代码);
  4. 发现有问题就用 openspec-explore 修改提案;
  5. openspec-explore "需求变更:xxx"(二次脑暴);
  6. openspec-propose "根据探索结果修改提案";
  7. openspec-apply-change(执行提案中变更内容);
  8. 【可选】openspec-verify-change(验证是否有未完成任务);
  9. openspec-archive-change(归档)。

场景 D:季度大清理

  1. openspec-bulk-archive-change --before 2023-12-31(批量归档)。

总的来说,上述相对来说还是比较繁琐,保持最简使用:想(openspec-propose)、做(openspec-apply-change)、收(openspec-archive-change) 即可。

四、多 Agent 协作:前后端并行开发

为什么需要多 Agent

SDD 文档生成完毕后,前后端的代码生成工作是相互独立的——前端根据前端 SDD 生成组件和页面,后端根据后端 SDD 生成 Controller/Service/Repository。这天然适合并行执行。

Cursor 中的多 Agent 协作

Cursor 支持多个 AI 编程模式并行工作,这是其核心优势之一。全栈开发场景下Tab 1 负责前端代码生成,Tab 2 负责后端代码生成,两个 Agent 同时运行、互不阻塞。

Claude Code 中的 Subagent 能力

Claude Code 内置了 Subagent(子代理)机制,适合命令行场景下的多任务并行。

Subagent 模式

Claude Code 提供了两种多 Agent 协作模式。(下个迭代再实践一下 Team 模式和普通 Subagent 的差别)

image.png

Subagent 配置与使用

Subagent 的核心配置项:

{  "description": "前端代码生成专家",  "tools": ["Read", "Edit", "Write", "Bash", "Grep"],  "permissionMode": "bypass",  "model": "sonnet",  "skills": ["前端编码规范"]}

全栈开发场景中的应用:

 Agent(你在对话的 Claude Code)  ├── Subagent 1:读取前端 SDD,生成前端代码       ├── model: sonnet       ├── tools: Read, Edit, Write, Bash       └── 任务:按照 tasks.md 生成前端组件    ├── Subagent 2:读取后端 SDD,生成后端代码       ├── model: sonnet       ├── tools: Read, Edit, Write, Bash       └── 任务:按照 tasks.md 生成后端接口    └── Subagent 3:(可选)生成接口 Mock 数据        ├── model: haiku        └── 任务:根据后端 SDD spec.md 生成 Mock

多 Agent 实践建议

image.png

五、前后端联调:Mock 数据与分阶段验证

三阶段验证策略

直接联调往往是效率最低的验证方式,推荐采用三阶段分离验证:

阶段 1:前端 Mock 验证  前端代码 + Mock 数据 → 本地跑通页面交互,验证 UI 逻辑阶段 2:后端独立验证  后端代码 → mvn clean compile → 构建通过 → 部署到测试环境阶段 3:前后端联调  前端连接测试后端接口 → 端到端验证

这样做的好处是:前后两端的问题可以提前发现、分别修复;避免在联调阶段才暴露;节省大量排查时间。

Mock 数据编写要点

Mock 数据质量直接决定前端自测的有效性,有三个关键要求:字段名和字段类型必须与后端 SDD 中定义的完全一致;参考已有接口的真实返回数据作为模板,而不是随意构造;覆盖边界场景(空列表、单条数据、多条数据、各字段极值如空字符串、超长字符串、null 值等)。

后端独立构建验证

后端代码不需要在本地完整启动整个 Java 服务,只需编译通过即可验证大部分代码问题。

# 切换到 Java 8 环境(根据项目实际 JDK 版本调整)sdk use java 8# 进入后端项目目录cd service-backend# 编译验证(无需本地启动整个服务)mvn clean compile

编译通过意味着:语法正确、依赖关系正确、类型兼容,是部署前最快速的验证手段。

前后端联调步骤

后端代码提交并部署到测试环境;前端本地开发服务通过代理配置,将 API 请求指向测试后端地址;前端请求携带功能路由标识,确保请求路由到对应的测试环境(而不是其他人的环境);逐接口验证,重点关注字段映射、状态处理、错误场景。

六、警惕 SDD 陷阱:测试如何介入全栈研发

SDD 不等于需求文档

这是 AI 全栈开发中最容易被忽视的问题。SDD 描述的是 "技术上怎么实现" ,而不是 "业务上所有的行为" 。AI 在模仿参考代码生成新代码时,会自动复刻很多隐性功能——这些功能在参考代码中存在,AI 认为是"理所当然"的,所以没有写进 SDD 文档,但实际上已经悄悄实现了。

隐性功能示例

示例 1:变量/表单清除(前端)

// AI 模仿欢迎语弹窗生成结束语弹窗时,自动复刻了"关闭弹窗时清空表单"的逻辑const handleClose = () => {  form.resetFields();   // ← 隐性功能:关闭时清空表单字段  setContentList([]);   // ← 隐性功能:清空内容列表状态  setVisible(false);};

示例 2:数据格式转换(后端)

// AI 模仿已有接口,自动添加了业务逻辑判断if (extendInfo.getIsPermanent()) {    extendInfo.setEffectiveDate(null);   // ← 隐性:永久有效时自动清除开始日期    extendInfo.setExpirationDate(null);  // ← 隐性:永久有效时自动清除结束日期}

示例 3:默认值补齐(后端)

// AI 自动实现了"优先级自增"逻辑,SDD 文档中未提及if (Objects.isNull(req.getSequence())) {    req.setSequence(getMaxSequence() + 1);  // ← 隐性:新增时优先级自动递增}

这些隐性功能可能正是需要的,也可能完全不符合当前需求。问题在于你不知道它们的存在。

测试介入建议

image.png

给测试同学的实操建议:把 SDD 文档当作起点,而不是终点。重点 Review AI 生成的代码,问自己一个问题:"参考功能有哪些隐性行为?这些行为在新功能中是否合适?"

七、综合效益与总结

实践效益

通过本文介绍的"Harness + SDD + 多 Agent"全栈开发方法论,在实际项目中验证的效益如下:采纳率提升,相比传统前后端分离开发,工作区模式可以很好地把项目需求上下文放到一起,更便于 AI 理解需求,设计编码;尤其通过Cursor的索引能力,进一步提高采纳率以及功能实现的完整性。耗时降低,SDD 模式下,AI 分析需求后产生两套 SDD 文档,使得前后端开发完全可以并行;以本需求为例,原本前后端2+4人日需求,在这种模式下,算上环境准备、踩坑时间、联调自测时间,压缩至3人日,提效50%+。调试环节不依赖阻塞,前端功能在全栈开发的视野下,已知数据结构可mock数据自测;后端功能通过远程调试的方式,支持本地打点调试;最终一并上测试环境验证,能够明确知道问题来自于前端还是后端;AI 全栈学习成本骤降,只需掌握入门级别前后端知识,即可介入简单全栈需求开发;提高业务域需求吞吐率。

方法论总结

本文介绍的全栈 AI 开发方法论,核心可以用一张图概括:

本文基于实际全栈开发项目经验整理,所有代码示例已脱敏处理,使用通用命名替代业务专有名词,如有问题欢迎交流探讨。

往期回顾

1.通用 AI Agent 驱动网关路由安全审计实践|得物技术

2.AI驱动:从运营行为到自动化用例的智能化实践|得物技术 

3.生成式召回在得物的落地技术分享与思考

4.立正请站好:一个组件复用 Skill 的工程化实践|得物技术

5.财务数仓 Claude AI Coding 应用实战|得物技术

文 /盖伦

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

低代码平台接入 Agent 后,我们踩到的组件、上下文和追问坑

作者 花椒技术
2026年5月20日 18:03

本文复盘一次内部低代码平台智能助手的阶段性验证。

目标很直接:用户在聊天入口里发送设计图、切图链接,或者一段文字描述,Agent 理解需求后调用低代码平台能力,生成页面或配置页链接。

早期简单场景可以跑通:

  • 一张图的规则页可以生成页面结果;
  • 单组件需求可以匹配组件并生成可编辑配置;
  • 熟悉平台的人可以通过补充组件描述,让 Agent 生成页面骨架。

但真实需求一复杂,问题也很快暴露出来:

  • 组件一多,生成结果开始不稳定;
  • 对话轮次一多,Agent 可能丢掉前面已经修改过的组件;
  • 遇到需要追问的场景时,系统可能把“追问内容”当成最终结果保存,反而丢掉前面已经生成出的有效结果;
  • 每次改 Prompt、组件描述或工具调用逻辑之后,缺少稳定的回归验证方式。

这次实践给我们的结论是:

低代码页面生成不是一次性的文本生成任务,而是一个带状态、带工具调用、带平台约束的工程流程。

所以它真正难的不是“让模型生成一次页面”,而是让生成结果能被平台理解、能继续编辑、能多轮修改、能追问补齐信息,并且每次调整后都能验证是否真的变好了。

1. 生成链路:它不是让 AI 自由画页面

低代码平台的核心资产不是一张空白画布,而是一批已经沉淀好的组件、参数、规则和配置能力。

所以这个 Agent 的定位不是“让 AI 随便画一个看起来差不多的页面”,而是给低代码平台增加一个自然语言入口。

整体链路可以简化成这样:

ig_08bea2cc5ff5fd15016a0d85e8ff7481919dabb65234f73f26.png

这个定位决定了后面所有工程取舍。

如果只是做 Demo,生成一个视觉上接近的页面就够了。但进入低代码平台之后,生成结果必须满足几个条件:

  • 只能使用平台真实存在的组件;
  • 组件参数要符合平台约束;
  • 生成出的页面配置要能被编辑器继续打开和修改;
  • 复杂场景里缺少信息时,要能正确追问;
  • 多轮修改时,不能把上一轮已经确认的组件改丢;
  • 后续要有测试样例验证生成质量。

换句话说,AI 的价值不是绕过低代码平台,而是更低成本地调用平台已有能力。

ig_092fc40abe7b5fc3016a0d6d81c0ec8191b0a82777166823f2.png

2. 第一阶段能跑通什么,边界在哪里

第一阶段验证里,比较稳定的场景集中在三类。

2.1 一张图的规则页

用户给出图片和描述后,Agent 根据输入生成对应页面。

这类页面结构相对固定,组件数量少,平台侧可选组件也比较明确,所以更容易生成可预览结果。

2.2 单组件页面

比如只需要一个弹窗组件、头图组件或者某个业务组件时,Agent 根据用户补充的少量参数,可以匹配组件并生成初始配置。

这个场景的关键是:任务边界足够窄。

它不需要同时判断很多组件之间的顺序、参数依赖和页面结构,所以更适合作为早期验证场景。

ig_092fc40abe7b5fc3016a0d6e9976188191a61522051481a09f.png

2.3 熟悉平台的人辅助生成骨架

如果使用者本身熟悉低代码平台,能明确告诉 Agent 页面里有哪些组件、每个组件承担什么功能,它就更容易先生成页面骨架,再由人工继续核改。

这个场景很适合内部提效,但它也提醒我们:当前能力还不是“无门槛自动生成复杂页面”,而是“在边界清楚的前提下,帮助人更快生成初版”。

可以把当前适用边界先收敛成一张表:

场景 当前适配度 原因
单图规则页 较高 结构固定,组件少
单组件页面 较高 任务边界清晰,参数较少
熟悉平台的人生成骨架 中等偏高 人能补充组件和参数上下文
多组件复杂页面 不稳定 组件选择、顺序、参数约束同时变复杂
长对话持续修改 不稳定 容易丢失已确认组件或误判任务边界

这一步判断很重要。很多 AI 项目早期最容易犯的错误,就是把 Demo 场景里的顺畅,误判成真实场景里的稳定。

3. 复杂需求上来后,问题从“生成”变成“状态管理”

真正的问题出现在复杂页面里。

真实页面通常不是单组件。一个活动页可能同时包含头图、规则、按钮、跳转、状态展示、弹窗、列表、业务卡片和各种配置参数。

组件一多,Agent 需要处理的就不只是“选一个组件填几个参数”,而是:

  • 哪些组件可以满足需求;
  • 多个组件之间是什么顺序;
  • 哪些参数必填,哪些可以默认;
  • 缺少信息时应该追问什么;
  • 用户补充信息后,应该更新哪个组件;
  • 新一轮输入是在改当前页面,还是开始一个新页面。

这时问题就从“能不能生成”变成了“状态能不能管住”。

3.1 坑一:追问时丢掉了已有中间结果

一次复杂需求里,用户希望生成一个包含头图、轮播视频图,以及两行两个主播的页面。

Agent 能理解大致方向,但落到实际配置时,还需要继续追问视频链接、封面图、主播数据、跳转信息等内容。

追问本身是合理的。

真正的问题在于,早期追问流程没有处理好:系统会把追问内容直接保存,反而没有保留前面已经搜索或生成出的有效结果,导致页面里的组件不正确。

从聊天视角看,这只是“问用户补充一个信息”。但从工程视角看,追问背后对应的是一个未完成任务、一部分页面结构,以及已经拿到的中间结果。

如果系统没有把这些状态保存住,追问就会变成覆盖。

后续修复后,追问时可以继续保留上下文,至少不会因为一次补充信息就把前面的有效结果丢掉。

ig_092fc40abe7b5fc3016a0d6f58e9a88191bbeab2834f0ff76c.png

3.2 坑二:多轮修改时丢弃已确认组件

页面生成不是一次性对话。

用户很可能会连续说:

  • 把头图换成另一张;
  • 第二个组件参数调一下;
  • 增加一个按钮;
  • 按钮跳转到另一个页面;
  • 再补一个弹窗。

如果前一轮刚确认的组件,下一轮因为一句新指令被覆盖掉,用户很快就会失去信任。

这个问题不能简单归因于“模型没理解”。更准确地说,是系统没有把当前页面结构、已确认组件、待补参数和本轮修改目标建模清楚。

3.3 坑三:任务结束判断不稳定

还有一个容易被低估的问题:一个页面需求什么时候算结束?

用户下一句话到底是在补充同一个页面,还是开始了一个新页面?

如果完全交给模型自动判断,实际效果会不稳定。当前更稳妥的方式,是通过手动结束会话来显式标记任务边界,避免系统误把不同页面认成同一个任务,或者把同一个任务拆成多个页面。

这类设计看起来不够“智能”,但在工程早期更可靠。

4. 四个必须收紧的工程边界

踩坑之后,我们发现问题不只是模型能力。

模型当然会影响理解和生成效果,但内部 Agent 要进入业务工具,必须先把工程边界收紧。

4.1 组件边界:不能让模型想象不存在的组件

低代码平台不是自由画布。

Agent 不能为了满足用户描述,想象一个平台里不存在的组件,也不能随意创造平台不支持的参数。

所以组件描述不能只写“这个组件是做什么的”,还需要补充:

  • 适用场景;
  • 不适用场景;
  • 必填参数;
  • 参数约束;
  • 示例输入;
  • 示例输出;
  • 缺信息时的追问方式;
  • 不支持时的返回策略。

组件说明对 Agent 来说不是普通文档,而是它选择工具、生成配置和处理异常时的重要依据。

4.2 上下文边界:页面生成需要任务状态

普通聊天可以模糊一点,但页面配置不行。

一个页面生成任务里,至少要记录:

  • 当前页面包含哪些组件;
  • 每个组件是否已确认;
  • 哪些参数还缺失;
  • 本轮用户输入作用于哪个组件;
  • 当前任务是否已经结束;
  • 下一轮是继续编辑还是新建页面。

如果没有任务状态,Agent 很容易在多轮对话里“看起来很努力”,但实际上把页面越改越乱。

4.3 工具调用边界:中间结果、追问和最终保存要分开

Agent 调用平台能力时,工具返回结果、用户追问、异常信息和最终保存结果应该有清晰流转。

尤其是追问场景,系统需要保留已经拿到的有效结果,而不是因为缺少某个参数,就把追问内容覆盖成最终结果。

一个更稳的流程应该接近这样:

识别需求
  -> 匹配组件
  -> 生成部分配置
  -> 判断缺失参数
  -> 保存中间状态
  -> 向用户追问
  -> 合并补充信息
  -> 再次生成 / 更新配置
  -> 最终保存

这里的关键不是流程图多漂亮,而是“中间状态”和“最终结果”不能混在一起。

4.4 验证边界:不能靠感觉判断这次变好了

页面生成不能只靠肉眼看一眼,也不能每次出问题都靠人工读日志、贴上下文、让代码 Agent 猜哪里错。

一旦日志变长、上下文变复杂,这种排查方式会非常低效。

更需要补上的是可回归验证机制。至少要覆盖几类样例:

  • 简单单图生成;
  • 单组件参数生成;
  • 多组件页面骨架生成;
  • 多轮修改不丢组件;
  • 追问后继续保留原有效结果;
  • 手动结束会话后不再误关联上一页;
  • 不支持组件时能正确拒绝或继续追问。

这样每次调整组件描述、工具调用逻辑或模型策略时,团队才知道它到底变好了,还是只是这一次碰巧跑通。

5. 一套更适合落地的迭代路径

这次验证之后,我们没有继续盲目扩大需求范围,而是先把问题收窄。

5.1 先做高频组件,不追求全组件覆盖

低代码平台里的组件很多,但真实高频使用的往往只是其中一部分。

早期与其追求全量覆盖,不如先把高频组件的描述、参数、示例和限制条件写清楚。

这一步的目标不是“看起来支持很多”,而是让最常见的场景先稳定下来。

5.2 把组件经验翻译成 Agent 能执行的规则

很多平台经验原本存在于研发、运营或熟练使用者脑子里。

比如:

  • 哪个组件适合规则页;
  • 哪个组件适合活动入口;
  • 哪些参数不能同时出现;
  • 缺少跳转信息时应该先问什么;
  • 哪些复杂需求只能先生成骨架,不能承诺全自动完成。

这些经验如果不显式写出来,Agent 只能靠模型推理去猜。

真正要做的是把这些经验翻译成结构化的组件能力说明和测试样例。

5.3 业务侧和平台侧分工要清楚

业务 Agent 接入平台时,业务侧和平台侧的边界要先分清。

低代码平台侧更应该关注:

  • 高频组件;
  • 组件能力说明;
  • 页面生成样例;
  • 失败样例;
  • 人工核改边界。

通用 Agent 平台更适合承接:

  • 会话状态;
  • 工具注册;
  • 日志分析;
  • 执行环境;
  • 权限审计;
  • 失败恢复;
  • 回归验证。

这样业务侧不用每次都从零搭一套 Agent 服务,而是把精力放回自己的业务能力描述、组件边界和验证样例上。

6. 可复用 Checklist

如果你也在做“Agent + 内部业务工具”或“AI + 低代码平台”,可以先用这份检查清单过一遍。

6.1 组件和能力描述

  • 组件是否有明确适用场景?
  • 是否写清了不适用场景?
  • 必填参数和可选参数是否区分清楚?
  • 参数之间是否有约束说明?
  • 是否提供了标准示例和反例?
  • 不支持的需求应该拒绝,还是继续追问?

6.2 多轮对话和状态管理

  • 当前任务是否有唯一上下文?
  • 页面里已确认的组件是否被保存?
  • 本轮修改作用在哪个组件上?
  • 缺失参数是否有单独状态?
  • 用户什么时候结束当前页面任务?
  • 新任务和旧任务如何隔离?

6.3 工具调用和结果保存

  • 工具返回结果是否和用户追问分开?
  • 中间结果是否会被追问内容覆盖?
  • 异常信息是否可追踪?
  • 最终保存前是否有必要校验?
  • 失败后是否能恢复到上一个有效状态?

6.4 质量验证

  • 是否有固定测试样例?
  • 是否能对比生成配置差异?
  • 是否记录失败原因分类?
  • 是否能复现多轮修改问题?
  • 是否能验证一次改动对多个场景的影响?

7. 总结:从能跑到可用,差的是工程闭环

如果只看第一次生成页面,AI 低代码页面生成很容易让人兴奋:图片丢进去,页面出来;一句话过去,配置页链接回来。

但工程团队真正要关心的是:

  • 复杂一点还能不能生成;
  • 多轮修改会不会破坏已有结果;
  • 缺信息时能不能正确追问;
  • 工具调用异常能不能恢复;
  • 生成结果能不能回到平台继续编辑;
  • 每次调整后有没有回归验证。

所以这次探索给我们的结论不是“AI 已经可以完全替代页面搭建”,而是更接近工程落地的一句话:

内部 Agent 想要真正可用,不能只追求模型更强,还要让业务规则、状态管理、工具调用和验证闭环一起成熟。

低代码平台里,组件和配置本身就是工程资产。AI 的价值不是绕开这些资产,而是更低成本地理解、调用,并把结果重新放回平台流程里。

先从高频组件开始,把简单页面做稳;再用测试和日志把问题收集回来,逐步扩大能力边界。这条路没有一次 Demo 那么漂亮,但它更接近真实工程落地。


花椒技术交流群

还在孤军研究 AI 工程化、AI 编程、Agent 落地,没人同行交流、没人拆解实战?

这里汇聚一线技术从业者,专注代码评审、企业内部 AI 助手真实实战落地。

想紧跟 AI 前沿动态、交流工程落地经验、少走踩坑弯路,欢迎直接加入「花椒技术交流群」。

群内专属福利拉满:每日精选研发向 AI 行业日报、文章独家延伸资料、文中未展开的技术细节,全部同步共享。

WechatIMG742.jpeg 如果群过期关注公众号 花椒技术,私信回复「交流群」获取最新入群二维码。

消失的 WWDC 愿望单 -- 肘子的 Swift 周报 #136

作者 东坡肘子
2026年5月19日 07:54

issue136.webp

消失的 WWDC 愿望单

距离 WWDC 2026 只剩下 20 天了。每年到这个时候,我都会看到不少开发者分享自己的 WWDC 愿望单,写下预测与期许。但今年,至少到我汇总本期周报时,这类内容相较去年同期明显少了许多。究竟是开发者对 WWDC 的期待变淡了,还是更多人开始秉持“降低预期才能获得更多惊喜”的心理?

也许问题并不是开发者没有期待,而是旧有的愿望单形式已经不太够用了。过去,我们期待的是某个 API、某个框架、某项功能;而现在,当软件开发迅速向 AI Agent 时代靠拢时,很多期待本身也变得更难被清晰描述。

一年前,恐怕很少有开发者会预料到软件开发会如此迅速地进入 AI Agent 时代。即便我们期待 Xcode 提供更好的 AI 支持,在 Xcode 26.3 推出前,也未必会想到苹果会在 IDE 中提供与 Agent 如此紧密的集成。应用或 API 已不再只是面向消费者或开发者的接口,它们也可能成为 AI Agent 理解、调用和编排的对象。AI 不只是开发工具,也会作为新的参与者,深度进入软件服务的构建和使用过程。

我想,这也是不少开发者面对 WWDC 2026 时既期待又茫然的地方。我们希望看到更新的功能、更稳定的框架、更清晰的平台方向;同时也在思考,在这样的开发体系中,如何继续保持自己作为开发者的独特性与必要性,并与 AI 一起构建更好的服务。

WWDC 2026 究竟会带来多少变化,让我们拭目以待。


BTW:上周我非常有幸入选了苹果官方最新公布的 Apple Developer Community Spotlight。作为一名内容创作者,能够得到这样的认可,对我来说既是鼓励,也是鞭策:继续认真写下去,继续把有价值的内容带给大家。感谢每一位长期阅读、反馈和支持我的朋友。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

从 URLSession 到电磁波:iOS 网络请求的底层原理 (URLSession to Electrons: How Networking works on iOS)

很多 iOS 开发者都知道:URLSessionDataTask 需要调用 resume() 才会真正开始请求。但在这之后究竟发生了什么?Jacob Bartlett 用一篇长文,带读者一路跟随一个普通的网络请求,从 URLSession、CFNetwork、TCP/IP、Wi-Fi,一直深入到无线电、天线与电磁波。Jacob 不仅串联起 HTTP、DNS、TCP、QUIC、IPv6 等开发者熟悉却未必真正理解的概念,也结合 iOS 内部实现介绍了 Network.framework、XNU 内核 TCP 栈、Wi-Fi 帧结构以及蜂窝网络的调度机制。

一个简单的 resume(),背后涉及的代码与协议演进跨度,可能超过十几甚至几十年。不得不感慨,现代软件世界建立在层层抽象之上,而绝大多数时候,我们其实只是幸运地生活在这些抽象足够稳定的年代。


当 AI 和 Xcode 打架时:我写了个工具来拉架

Xcode 的构建系统从未真正为“并行开发”设计过。多个任务同时构建时,DerivedData、ModuleCache、SwiftPM 缓存乃至 Simulator 都可能互相踩踏,而这一问题在 AI Agent 并行开发场景下被进一步放大。Maples7 在本文中介绍了他的解决方案:VibeChard。它基于 Git Worktree,为每个 AI Agent 创建独立的构建沙箱,并进一步隔离 DerivedData、ModuleCache、SwiftPM 缓存以及模拟器环境。最有意思的是,它并不要求开发者修改构建命令,而是通过 PATH shim 透明接管 xcodebuild,让包括 Tuist、Fastlane 在内的整条工具链都自动运行在隔离环境中。与其说这是一个单纯的辅助工具,不如说它揭示了 AI 编程时代一个更底层的问题:当代码生成速度大幅提升后,传统开发工具链的环境隔离能力也必须随之升级。


Swift 真的被搞得乱七八糟了吗?写了几年之后说点实话

Swift 变得越来越复杂,这是一个不争的事实。但这是否意味着 Swift 已经变成了一门糟糕的语言?迷途酱 从语言演进的现实约束出发,重新审视 Swift 这些年的“膨胀”。作者认为,许多被吐槽的复杂度,其实来自 Swift 同时承担应用层、系统级、DSL 宿主与服务端语言等多重目标,而并发安全与所有权模型本身,也属于计算机科学层面的“硬复杂度”。

文章既讨论了 Sendableactor isolationborrowing~Copyable 等近年来快速增长的新概念,也坦率指出 Swift 在并发关键字、泛型语法以及 SwiftUI “魔法感”上的设计问题。尤其是“Swift 其实是一个语言的多个层级”这一观点,相当值得思考:绝大多数开发者日常写 App 时,其实并不需要承担全部复杂度,但 WWDC 与官方文档却经常把这些内容同时呈现在所有人面前。


Xcode Cloud 进阶:Shell 脚本自动化实战 (Writing shell scripts for Xcode Cloud)

Xcode Cloud 的上手体验已经足够简单,但在真实项目中,许多自动化需求仍然需要借助 shell scripts 完成。Amy Delves 以“在归档完成后自动创建 GitHub Release”为例,展示了如何结合 Xcode Cloud 提供的环境变量判断当前构建是否来自 main 分支的归档流程,如何读取 archive 中的版本号与构建号生成 tag,并通过 GitHub API 创建 release。虽然示例本身并不复杂,但它很好地说明了 Xcode Cloud 并不只是一个“点几下就能跑测试”的服务:借助环境变量、脚本钩子与外部 API,它同样可以承担更完整的发布自动化工作流。


我们为什么离开了很棒的 CloudKit (Why CloudKit is amazing and why we're leaving it)

这是一篇相当少见、坦诚的 CloudKit 迁移复盘。César Pinto Castillo 并没有简单批评 CloudKit,恰恰相反,他首先承认:对于小团队来说,CloudKit 几乎提供了一套“不可思议”的能力组合——免费同步、自动身份认证、端到端加密、无服务器运维,以及跨 Apple 平台的共享能力。

但随着产品逐渐发展,CloudKit 的另一面也开始显现:缺乏服务端可观测性、Schema 发布依赖手动操作、不同 Apple 平台间长期存在的同步边缘问题、对 iCloud 账户的强依赖,以及最关键的——无法真正走向 Web 与跨平台生态。最终,César 所在的团队将数据迁移到了基于 Supabase/Postgres 的同步架构。

CloudKit 是苹果生态最重要的护城河之一。但在应用越来越复杂、数据规模越来越大、用户对同步实时性的要求越来越高的今天,它的能力边界也开始显现。在追求“无感同步”的同时,我想苹果也确实需要重新认真审视这个多年未发生明显演进的基础设施了。


用 Swift 手写 LLM 训练内核 (Training an LLM in Swift, Part 1: Taking matrix multiplication from Gflop/s to Tflop/s)

在 Swift 中训练 LLM 听起来多少有些“匪夷所思”,但 Matt Gallagher 做了一次非常硬核的性能探索:不依赖现成机器学习框架,而是从手写矩阵乘法开始,一步步将基础 Swift 实现从 2.8 Gflop/s 优化到 1.1 Tflop/s。文章通过一个完整的优化过程,展示了高性能 Swift 的现实面貌:Swift 并不是不能快,但当你逐渐逼近硬件能力时,也将不可避免地进入 UnsafeBufferPointer、SIMD、并发切片、内存布局与 GPU tile 优化的世界。

这篇文章的价值并不在于鼓励开发者手写机器学习内核。恰恰相反,作者反复强调,生产环境应该优先使用 Accelerate、BNNS、Core ML、MPSGraph 等成熟框架。

当 Swift 被优化到接近 C 的程度时,它还能否保持原本的可读性与优雅?这篇文章给出了一个非常具体,也很诚实的答案。


Swift 适合写 App,但不适合训练 ML 模型 (Swift Is Great for Apps, Not for Training ML Models)

上一篇文章还在展示如何将 Swift 手写矩阵乘法一路优化到 Tflop/s,这篇文章则从另一个角度泼了盆冷水:Mohammad Azam 认为,Swift 与 Core ML 非常适合“部署”机器学习模型,但并不适合承担现代机器学习训练流程本身。Mohammad 指出,真正耗费时间的往往不是模型训练,而是数据清洗、特征工程、归一化、Pipeline 组合与实验迭代,而这些恰恰是 Python 生态最成熟、最顺手的领域。

Swift 并非不能触碰机器学习底层,但当问题从“性能”转向“数据科学工作流”后,语言与生态的重心差异便会迅速显现。这也凸显了 Swift 当前的一个困境:它具备进入多个领域的语言能力,但在应用开发之外,配套生态仍不足以支撑同等顺畅的开发体验。

工具

Swift MarkdownEngine

MarkdownEngine 是 Nodes 团队从自家 macOS Markdown 应用中抽离并开源的原生编辑器引擎。它不是 HTML 渲染器,也不是 WebView 包装,而是基于 TextKit 2 与 AppKit 构建,并桥接到 SwiftUI 的 source-style Markdown 编辑器:文本仍保持纯 Markdown,但在编辑时提供类似 Obsidian Live Preview / iA Writer 的实时样式。项目支持 wiki link、图片嵌入、代码块高亮、LaTeX、任务列表、Writing Tools,以及针对代码、公式和链接的拼写检查抑制。

TextKit 2 文档稀薄、行为细节多,而这个项目把一套已在 Nodes.app 中使用的编辑器能力开源出来,对正在开发写作、笔记或知识管理类 macOS 应用的开发者很有参考价值。


Harness:让 AI 像真实用户一样测试你的 App

Harness 是由 Alan Wizemann 开发的一款原生 macOS 开发者工具,可以驱动 iOS Simulator、macOS App 和 Web App。你用自然语言写下目标,选择一个 persona,Harness 会让 LLM agent 基于截图观察界面、执行点击和输入,并生成可回放的运行路径、成功或失败结论,以及按类型记录的 UX friction。相比单纯让 AI “操作应用”,它更像是把 AI agent、截图、事件日志、凭证脱敏、运行回放和摩擦分类整合成了一套面向开发阶段的用户测试工作台。

目前项目仍处于 alpha 阶段,Web 端依赖 WebKit,iOS/macOS 的 Set-of-Mark 定位能力还在规划中,因此更适合用于探索产品体验中的模糊点和死角,而不是替代确定性的回归测试。不过从 Swift 6、SwiftUI、SwiftData、actor 化的执行流程、JSONL run log,以及跨 Anthropic/OpenAI/Gemini 的模型抽象来看,它已经不是一个简单 demo,而是一个很值得观察的 AI-native developer tool 样本。

传统 UI 测试更擅长验证开发者预设好的路径,而 Harness 试图回答另一个问题:一个带着具体目标和身份设定的真实用户,会不会在你的界面中顺利完成任务。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

AI时代前端摸鱼必备,20秒将psd还原成页面,支持HTML / React / Vue

作者 miaowmiaow
2026年5月7日 11:01

设计稿来了,运营要求"明天上线"。 你打开 PSD,开始切图、量像素、写 CSS、对位置——半天过去了,还在调那个差 2px 的按钮。

这篇文章介绍我们自研的 psd2code 工具:一行命令把 PSD 转成可运行的前端项目,像素级还原 + 智能布局优化 + 多框架产物(HTML / React / Vue)


一、为什么不用现成的 PSD 转 HTML 工具?

社区里其实已经有不少 PSD 转代码方案,但落到运营活动 / H5 / 长图详情页这类「像素级还原」需求上,普遍有三个痛点:

痛点 现象
① 文字字号不准 PSD 里的 FontSize=17.5,浏览器渲染出来又小又模糊——因为忽略了图层 transform.scale
② 全是 position: absolute 几百个图层全部绝对定位,CSS 体积爆炸,后期完全不可维护
③ 组级效果丢失 圆角矩形 8px 外描边、文字描边+投影叠加,要么裁切要么糊掉

我们做了一组对比统计(实际 PSD:南瓜大作战 H5、总决赛-折叠 H5、兑奖 H5):

传统切图工作流:     设计稿到可运行 HTML 平均  4~6 小时
psd2code 自动转换: 设计稿到可运行 HTML 平均  20

由此得出 psd2code 的四大核心方向,也正是本文后续四大章节:

  • PSD 解析:借助 psd-tools,但对它的缺陷做深度修补;
  • 资源提取与优化:像素去重 + 智能命名 + 合成背景图;
  • 布局优化:聚类算法识别行列/网格、智能重写成 Flex;
  • 多 Target 可插拔:同一份 IR,一键产出 HTML / React / Vue 三种工程。

二、整体架构:编译器式分层

psd2code 借鉴编译器的「前端解析 + IR + 后端代码生成」三段式:

flowchart LR
    PSD[".psd<br/>设计稿"] --> Core["core/<br/>PSD 解析 + 图层渲染"]
    Core --> IR[("IR<br/>pydantic 校验<br/>的中间表示")]
    IR --> HTML["targets/html<br/>HTML + CSS"]
    IR --> React["targets/react<br/>Vite + React 18"]
    IR --> Vue["targets/vue<br/>Vite + Vue 3"]
    IR -.预留.-> MP["targets/mini-program"]

    style PSD fill:#f9e79f,stroke:#b9770e
    style IR fill:#aed6f1,stroke:#1f618d
    style HTML fill:#d5f5e3,stroke:#196f3d
    style React fill:#d5f5e3,stroke:#196f3d
    style Vue fill:#d5f5e3,stroke:#196f3d
    style MP fill:#f5f5f5,stroke:#999,stroke-dasharray: 5 5

核心抽象

  1. IR (Intermediate Representation):pydantic BaseModel 严格定义、自带校验。是 coretargets 之间的契约——任何 target 都从 IR 出发,不直接读 PSD
  2. PipelineContext:贯穿所有 Stage 的全局上下文,承载 PSD、IR、配置、产物路径、target 中间产物等。
  3. Stage:单一职责的处理步骤,输入/输出都是 PipelineContext。
  4. Target:一个产物对应一个 Target 子类,通过 @register("html") 注册到全局 registry。

这个分层带来一个直接好处:HTML target 每次能力升级,自动惠及 React / Vue target——因为后两者只是在 HTML 产物之上做二次加工。


三、Skill 使用方式

psd2code 同时也是一个 CodeBuddy Skill,对话里直接说"帮我把这个 PSD 转成 HTML"就会自动触发;也可以脱离 CodeBuddy 单独跑命令行。

3.1 在 CodeBuddy 中调用(推荐)

只要项目里有 .codebuddy/skills/psd2code/ 目录,触发词就能让 CodeBuddy 自动加载该 skill:

"帮我把 设计稿/南瓜大作战.psd 转成 HTML" "把这个 psd 转成 React 项目" "psd 转 vue" "设计稿转代码"

CodeBuddy 会自动选择合适的 target、定位 PSD 文件、执行 skill、把产物路径回报给你。

3.2 命令行直接运行

# 默认 target = html(同时产出 absolute 原版 + Flex 优化版)
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd

# 显式指定 target
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd --target html
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd --target react
python3 .codebuddy/skills/psd2code/psd_to_code.py /path/to/file.psd --target vue

3.3 常用参数

参数 默认 说明
--target {html,react,vue} html 选择产物形态
--css-style {compact,expanded} compact 优化版 CSS 输出风格:compact 接近手写、expanded 全展开 + PSD 坐标溯源注释
--no-css-pretty 关闭 关闭 CSS 美化,回到字母序机械渲染(CI 基线对比常用)

举例:

# 想要排查某个元素位置不对,开 expanded 模式看坐标溯源注释
python3 .codebuddy/skills/psd2code/psd_to_code.py 南瓜大作战.psd --css-style expanded

# 跑 React 产物 + 启动 dev server
python3 .codebuddy/skills/psd2code/psd_to_code.py 南瓜大作战.psd --target react
cd output/南瓜大作战/react
npm install && npm run dev    # http://localhost:5173

# 跑 Vue 产物
python3 .codebuddy/skills/psd2code/psd_to_code.py 南瓜大作战.psd --target vue
cd output/南瓜大作战/vue
npm install && npm run dev    # http://localhost:5173

3.4 产物目录速查

output/<psd_stem>/
├── html/                        # 任何 target 都会先产出
│   ├── index.html               # 原始 absolute 版(保留 dev metadata,方便诊断)
│   ├── index_optimized.html     # Flex 优化版(已剥离 dev metadata,最终交付物)
│   ├── style.css / style_optimized.css
│   ├── main.js                  # 国际化等运行时逻辑
│   ├── metadata.json            # 图层树元数据
│   ├── layer_map.json           # 反查表:CSS 类名 → PSD 原图层名
│   ├── _naming_report.md        # 语义命名报告(每个 token 的来源)
│   └── images/                  # 切图 / 合成图 / 背景图
├── react/                       # --target react 时产出
└── vue/                         # --target vue 时产出

3.5 排查与定位三件套

跑完后如果发现某处不对,优先看这三个文件

  • _naming_report.md:CSS 类名为什么是这个?哪一层(layer1 词典 / layer2 角色推断 / fallback 拼音)给的?
  • layer_map.json.bg-main-4e8c1d 是 PSD 里的哪个图层?图层类型?
  • 对比 index.htmlindex_optimized.html:absolute 原版作为"地面真相",优化版有偏移基本就是 LayoutOptimizer 哪一步过激了。

3.6 系统依赖

Python 3.10+
psd-tools >= 1.14
Pillow >= 10
numpy
beautifulsoup4
pydantic >= 2.0
pypinyin

四、PSD 解析:踩过 psd-tools 的那些坑

psd-tools 是 Python 生态里最成熟的 PSD 解析库,但它只实现了最常用的渲染器,对一些常见场景(shape 填充 + 图层样式、超大发光溢出、引擎字典 transform 等)要么出错、要么丢失。我们的做法是:能修的源码级修,不能修的绕开走手动栅格化

3.1 文字 transform.scale 修正

PSD 文本图层的 engine_dict.StyleRun...FontSize原始字号,但 PS 实际渲染时会用 layer.transform = (a, b, c, d, e, f) 矩阵缩放——其中 a 是 X 缩放、d 是 Y 缩放。

font-size-fix.svg

真实例子:

PSD 图层:"消耗99兑换币"
  raw FontSize    = 17.5
  transform.scale = 1.6
  实际渲染字号     = 17.5 × 1.6 ≈ 28px

如果忽略 transform.scale,浏览器会用 17.5px 渲染,文字直接缩成花生米。另一个易踩的兄弟坑:ParagraphSheet 的路径不在 StyleRun 下,而是 engine.ParagraphRun.RunArray[0].ParagraphSheet.Properties 里的 Justification,老代码写错路径会导致所有文字永远左对齐。这两处我们在 core/psd/text_extractor.py 里重新解析。

3.2 浏览器字宽差异:纵向 + 横向双兜底

PSD 设计师常用思源黑体 / 造字工房,浏览器渲染时却用 PingFang / Arial——相同字号下,浏览器中文会比 PSD 更宽,导致:

  • 单行文本被挤成两行("预测四"+"强")
  • 按钮内文字撑出按钮边界

effect-comparison.svg

我们设计了双兜底:

# 纵向兜底:字号不超过 bbox 高度的 0.85
if font_size >= height * 0.85:
    font_size = height * 0.85

# 横向兜底(仅纯中文短标题):按字数 + 宽度倒推上限
if pure_cjk and char_count <= 12:
    max_font_by_width = width / cjk_count * 0.95
    font_size = min(font_size, max_font_by_width)

效果验证(总决赛-折叠 PSD):

文本 修正前 修正后 效果
主赛区(68px 宽) 25.5px 20.9px ✓ 单行
预测四强(164px 宽) 46.75px 38.5px ✓ 不再折行

3.3 Shape + 图层样式描边:psd-tools 的致命 bug

一个看似普通的 PSD shape 图层(#feffd7 浅黄填充 + 2px 内描边 #5f8618 绿色),psd-tools 直接 layer.composite() 会得到整片纯绿色——填充色完全丢失。

排查后发现两个叠加的根因:

  1. layer.topil() 对 shape 返回 None,老代码降级到 layer.composite() 取基础图;但 composite() 把整块 shape 区域错误地涂成描边色。
  2. draw_stroke_effectskimage.filters.scharr 检测 alpha 边缘做描边,当 alpha 紧贴画布无 padding 时,scharr 检测不到边缘,归一化后 mask 整片=1,描边色铺满整张图。

绕开方案

  • 新增 _render_shape_base_from_fill(layer):跳过 composite(),直接读 SoCo 填充色 + origination 几何(Rectangle / RoundedRectangle / Ellipse)用 PIL ImageDraw 合成基础图。
  • 调用 draw_stroke_effect 前给 alpha 加 padding(pad = max(stroke_size+2, 4)),渲染完再裁回。
# stroke_renderer.py
padded_alpha = np.pad(alpha, ((pad,pad),(pad,pad),(0,0)), mode='constant')
stroke_color, stroke_mask = draw_stroke_effect(bbox, padded_alpha, ...)
out[:,:,:3]  = stroke_color[pad:pad+h, pad:pad+w, :]
out[:,:,3:4] = stroke_mask[pad:pad+h, pad:pad+w, :] * opacity

效果:兑奖活动卡片的浅黄背景 + 2px 绿描边正确还原,无需人工补图。

3.4 图层样式的两层 enabled 开关

PS 图层样式面板有两个开关:

  • 整体开关layer.effects.enabled(fx 行最左边那个勾)
  • 单项开关effect.enabled(外发光/描边/投影各自的勾)

PS 的规则:整体开关关闭 → 所有效果都不渲染,哪怕单项 enabled=True

psd-tools 不替你 AND 这两个标志,psd2code 早期代码多处只看 effect.enabled,导致"PS 中没效果的文本"被当成"有外发光"处理,错误地栅格化成图片。修复后统一用一个 helper:

def is_effect_active(effect, layer):
    if not layer.effects or not layer.effects.enabled:
        return False
    return bool(effect.enabled)

全工程 8 处调用点全部切换。这解决了带 fx 残留的昵称文本全部被错误降级为图片的问题。

3.5 组级效果溢出:手动渲染 + composite 混合

PS 图层效果(描边、阴影、发光,共 8 种)在组(Group)级别有个隐蔽特性:效果会沿组的整体边界裁切

psd-toolscomposite() 能在组内正确复现这一行为——但只在组的 bbox 内有效。一个圆角矩形有 8px 外描边时,描边会溢出组的 bbox 被 composite() 裁掉。实测过各种"绕过"方法——父级 composite、根级 composite、给超大 viewport——都是徒劳:

psd-tools 在任何层级 composite,都按目标节点(及其所有祖先组)的 bbox 做硬裁切,不存在绕过方案

我们的解法是「手动栅格化 + composite 混合」:

hybrid-render.png

1. 先用扩展画布 + 手动逐层渲染 → 拿到完整溢出像素(外部区域)
2. 再用 group.composite(viewport=bbox) → 拿到组 bbox 内的 PS 原生高质量
3. 把 composite 结果覆盖到手动渲染结果的内部区域

最终:外部保留溢出效果,内部达到像素级匹配(实测 Alpha 差异 max=0、mean=0.00)。

硬约束:子组(嵌套组)必须用 sub_grp.composite(viewport=grp_bbox) 渲染,不能退回"递归调用 _render_group_expanded + 裁切"——历史回归实测会在圆角轮廓位置多出 ~75px/行的错误描边。

3.6 总结:PSD 解析的修复清单

问题 表现 我们的做法
transform.scale 未应用 文本被缩成花生米 layer.transform[0],FontSize 乘以它
ParagraphSheet 路径错误 所有文字都是左对齐 ParagraphRun.RunArray[0] 路径重新解析
shape + 图层样式整片涂描边色 兑奖卡片全变绿 手动栅格化填充 + 给 alpha 加 padding 再描边
两层 enabled 未 AND 无效果文本被错误降级为图片 统一 is_effect_active(effect, layer)
组级效果溢出被裁 外描边断掉 混合渲染:手动扩展 + composite 覆盖

五、资源提取与优化

psd2code 对每个图层做一次决策:切图 / 文字保留 / shape 用 CSS 还原 / 吸收为父容器背景 / 合并成单图,最终写到 output/<psd>/html/images/ 目录。

5.1 智能去重:基于内容哈希而非图层名

活动页里"星星""糖果""装饰点"这类小图会被设计师复用几十次,每次都单独切图是极大浪费。

def _save_image_dedup(self, img, name, depth) -> str:
    data = serialize(img)                        # 按 Config.IMAGE_FORMAT 序列化
    md5_hash = hashlib.md5(data).hexdigest()
    if md5_hash in self._image_hash_map:         # 命中去重
        return self._image_hash_map[md5_hash]
    path = make_image_filename(name, content_hash=md5_hash, ltype=ltype)
    self._image_hash_map[md5_hash] = path
    write(path, data)
    return path

南瓜大作战 H5 实测:239 个 image 图层 → 87 张 PNG(去重率 63.6%)。

5.2 语义化文件名:tag + 内容哈希

旧方案拼音 + 自增序号(yuanjiaojuxing_3_7.png)有两个问题:HTML 里不可读;每次运行序号都在跳,git diff 噪声极大。

新方案:

images/<semantic-tag>-<md5前6位>.png

例:rounded-a3f012.png
    btn-receive-279914.png
    bg-main-4e8c1d.png
    candy-big-7b0a12.png
  • semantic-tagsemantic/ 子包从图层名推断得出(支持 3 层置信度:Layer2 角色推断 → Layer1 清洗词典 → fallback 关键词 + 拼音),PS 默认名走 ltype 兜底(img/shape
  • md5前6位 = 图片内容哈希——PSD 没改,文件名就不变,git diff 和 CDN 缓存两全其美
  • 同名撞车自动追加 -2/-3

5.3 形状层保留矢量:不切图就是最好的优化

圆角矩形、椭圆、纯色矩形这类简单 shape,不切图而是直接翻译成 CSS 几何属性

PSD shape 输出 CSS
Rectangle + SoCo 填充 background: <color>; width/height
RoundedRectangle border-radius: <r>px
Ellipse border-radius: 50%
shape + 图层样式描边 border: <w>px solid <color>

效果:CSS 体积下降的同时,文件也能 retina 无损缩放。

5.4 多张全屏背景的合成

活动页常见模式:组里有 2~3 张全屏背景叠加(渐变底 + 花纹 + 噪点)。如果每张都单独切图,HTML 里会写多 url 背景:

.bg-section {
  background-image: url(bg-gradient.png), url(bg-pattern.png), url(bg-noise.png);
  background-position: 0 0, 0 0, 0 0;
  background-repeat: no-repeat, no-repeat, no-repeat;
}

问题:多张 PNG 多次请求,而且浏览器要多次合成。psd2code 的做法是在布局优化阶段检测这种多 url 模式,用 PIL alpha_composite 合成单张 PNG 写回磁盘

输入:bg-gradient.png(284 KB) + bg-pattern.png(412 KB) + bg-noise.png(67 KB)
输出:flat-af0dce35.png (153 KB)   # 1/5 体积、1 次请求

南瓜大作战 H5 实测:47 组合并、节省 45.6 KB。

一个与 CSS 规范相反的坑:background-image: url(a), url(b) 中第一个 url 在视觉最上层,而 PIL alpha_composite 期望"底层在前"。调用方必须 reverse 列表——早期代码漏掉 reverse 导致所有合成图的颜色上下层叠错,颜色"对调"。

5.5 图层扁平化:子图合并 + 父容器吸收

更激进的优化:当一个容器里只有纯 image 子图层(无文本、无按钮),把容器自身的 background + 所有 image 子按 z 序合成单张 PNG、删掉所有子 div 及其 CSS 规则、只留容器自己的 background-image

这个 ImageLayerFlatten transformer 采用后序遍历 + 多轮扫描:最深层先合并、外层再发现"我的子变成单 div 了"继续合并。护栏非常严格:

  • 子元素必须全部 data-type="image" 且无孙子
  • opacity≈1 / mix-blend-mode: normal
  • 容器本身不能有 border-radius / box-shadow / clip-path / filter / transform 等"不能烧进 PNG 的装饰字段"(一旦合并后再叠这些,会双重作用)
  • 总层数 ≥ 2,几何包围盒 ≤ 画布 50%(否则合并一张巨图反而得不偿失)

南瓜大作战 H5 实测:这一步搞定了 47 个容器的视觉简化,DOM 节点从 ~500 降到 ~280。


六、布局优化(本工具最核心的功能)

直接用 absolute 还原 PSD 没问题,但 200+ 图层全部 position: absolute 是工程灾难。psd2codeLayoutOptimizer 把 absolute 智能重写成 Flex,同时保证视觉零偏移。

flex-before-after.png

6.1 七步流水线全景

flowchart TD
    A["原始 absolute HTML"] --> B["Step 1:DOM 重构<br/>(聚类 / 背景剥离 / 容器吸收)"]
    B --> C["Step 1.2:图层扁平化<br/>(多 image 子 → 单张合成 PNG)"]
    C --> D["Step 1.5:同质兄弟分组<br/>(识别 v-list,支持 v-for)"]
    D --> E["Step 2:Flex 推断<br/>(analyzer V10 + 三道闸门)"]
    E --> F["Step 2.5:单子 wrapper 折叠<br/>(消除中间层)"]
    F --> G["Step 3:CSS 去冗余<br/>(z-index 精简 + 等价规则合并)"]
    G --> G2["Step 3.5:重复元素抽取<br/>(3+ hash 类 → 单 base 类)"]
    G2 --> H["Step 4:CSS 美化<br/>(DOM 序 + 属性分段 + 多行)"]
    H --> I["✅ 优化后 HTML / CSS"]

    style A fill:#fadbd8,stroke:#c0392b
    style I fill:#d5f5e3,stroke:#196f3d
    style B fill:#fcf3cf,stroke:#b7950b
    style E fill:#fcf3cf,stroke:#b7950b
    style G fill:#fcf3cf,stroke:#b7950b

6.2 聚类算法:怎么"看懂"一堆 absolute 框

这是整个 LayoutOptimizer 的灵魂。对任意一个容器,我们有 N 个子图层的 bbox(left/top/width/height),目标是自动把它们组织成**行(row)/ 列(col)/ 叠图组(stack)**的树状结构。

第一步:切行(_split_by_rows

从左到右、从上到下遍历子元素,维护一个"当前行"的 envelope(bbox 包络)。新来一个元素 e,判断它和 envelope 的纵向重叠率

overlap_y = min(e.bottom, env.bottom) - max(e.top, env.top)
ratio     = overlap_y / min(e.height, env.height)

if ratio >= 0.5:  # 同行判据
    env 吸收 e,继续
else:
    新开一行

第二步:行内切列

对每一行内部,把切行逻辑换成纵/横轴就是切列。递归后我们得到一棵"行包列 / 列包行"的嵌套树。

第三步:背景层剥离

一个组里常有"全屏卡片底框 + 多个浮层元素"的设计模式。直接聚类会把底框当成一个"占 100% 空间的大元素",严重干扰行/列判断。我们在聚类前先剥离满足以下三种规则之一的"背景层":

  • 完全包含型:bbox 完全包住其他所有元素
  • 主轴覆盖型:在主轴(宽或高)上覆盖 ≥ 90%
  • 双轴主导覆盖型:宽、高都覆盖 envelope ≥ 80%(识别"略带 padding 的卡片底图")

剥离后的背景层被吸收进父容器的 background-image 列表。

第四步:伪多行装饰堆叠回退

切出多行后,若所有行都只有一个元素、且相邻行横向覆盖率 ≥ 80%,回退为 stack(堆叠)——这是"图标 + 标题上下贴边"这种"本质上堆叠装饰"的场景。

第五步:二维网格识别

当多行 × 多列的元素满足"列对齐 + 跨行对齐"时,单纯用"列 包 行"嵌套表达不够干净,改成显式的 v-grid-row + flex column 结构:

rows = _split_into_rows(...)
if len(rows) >= 2 and all_rows_have_aligned_cols(rows):
    layout_type = 'grid'
    flex_applier 包装为:
      父: display:flex; flex-direction:column
      每行: <div class="grid-row-N v-grid-row">

南瓜大作战 H5 的"用户信息区"9 个子图层(剥掉背景卡 + 头像装饰后剩 7 个文本),被正确识别为 2 行 × [4, 3] 列 grid。

6.3 三道安全闸门:什么时候不该用 Flex

不是所有看起来"整齐"的容器都该用 Flex。我们踩过太多坑后总结出三道闸门(全在 layout_analyzer.py):

互相重叠的装饰簇

n 个图层互相重叠(每个与多个邻居都重叠),且 trend_ratio < 0.6。典型场景:多层装饰贴纸、若干徽章叠在一起。判定为堆叠装饰,保持 absolute。

支配背景层

存在某个子元素 X 满足 X.area / envelope.area >= 0.8,且其余子元素中 ≥ 60% 显著落在 X 内(重叠/自身面积 ≥ 0.6)。判定为"大底图 + 多个浮层"的卡片,整组保持 absolute。

装饰剥离

先把子节点分类为 bg / decor / content 三类,只在 content 子集上做趋势检测。这让"内容整齐成行 + 角落有装饰"的容器不再因为装饰打乱排版被误判。

6.4 Flex 应用:非趋势子元素保留 absolute

识别为 vertical / horizontal 后,我们把趋势元素写成 flex 子项(用 margin 表达间距),非趋势元素(角标、装饰)保留其 position: absolute 坐标:

/* 趋势元素:flex 流 */
.prop-card-1 { margin-top: 20px; margin-left: 0; }
.prop-card-2 { margin-top: 18px; }

/* 非趋势元素:保留 absolute */
.badge { position: absolute; right: -6px; top: -6px; }

这里有个极易反复重犯的 bug:容器重构后,子元素的 top/left 是"相对父容器"的坐标(由 extract 阶段产出),不需要再减父 top。

还有一条来自 v-stack 的保护:flex_applier 默认会 del child_css['position'] 把子元素的定位去掉;但如果子本身是 v-stack wrapper(内含 absolute 子节点),删除 position 会让其孩子跳到外层定位,直接飘到屏幕角落。修复:遇到 'v-stack' in child.classes 就改成 position: relative,而不是删除。

6.5 同质兄弟簇检测:识别"同类卡片"

PSD 设计师经常把 N 个商品卡 / 道具卡 / 礼包卡平铺在 #canvas 直接子,没有用父组包起来。传统聚类只在已有 group 内部做,这种列表会全部走 absolute 路径,开发拿到的 HTML 完全看不出"它是一个数据列表",没法直接写 v-for

SiblingGroupDetector 的 5 条 AND 规则:

  1. ≥ 3 个连续兄弟
  2. class 词根相同(去掉 __\d+ 后缀和 -\d+ 序号)——prop__30 / prop-2__38 / prop-10__101 词根都是 prop这是最强的设计师意图信号
  3. bbox 尺寸近似(误差 ≤ 5%)
  4. 满足网格规则:M 列 × K 行 满格排布,同列 left 一致、同行 top 一致(误差 ≤ 2px)
  5. 父容器本身不是 flex

识别成功后包成虚拟容器:

<div class="prop-list v-list" data-virtual="list">
  <div class="prop__30 layer-group">...</div>
  <div class="prop-2__38 layer-group">...</div>
  <div class="prop-3__45 layer-group">...</div>
</div>

CSS 用 display: flex; flex-wrap: wrap; column-gap / row-gap,下游开发可直接写 v-for

一个设计决定:我们不做子结构同构判定。实际 PSD 里同类卡几乎总是有差异(首张卡设计完复制改文案,结构漂移:少一行文字、按钮换成图片、装饰数量不一致)。强求子结构一致会绝大多数现实场景识别失败——class 词根 + bbox 尺寸两条已经够强。

6.6 CSS 去冗余:z-index 精简 + 等价规则合并

core/extract/layer_exporter.py 给每个图层无脑塞 z-index = 全局 layer_id——这是合理的像素还原默认值,但优化版完全不需要。CssDedup 分两个 Pass:

Pass 1 — z-index 精简

遍历每个父容器,收集子元素的 (selector, z) 序列:

形态 动作
长度 0 跳过
长度 1(独 z,其他全 None) 删该 z-index
长度 ≥ 2 严格递增 全删(DOM 顺序 = z 序)
长度 ≥ 2 出现倒挂 全保留

逻辑:position:absolute 子元素的叠序只在"兄弟 bbox 重叠"时依赖 z-index;绝大多数父容器下"DOM 顺序 = z 序升序"(这是 LayerRenderer 的天然产出),浏览器默认行为就能正确实现叠序。

Pass 2 — 等价规则合并

属性 dict 完全相等的多个选择器合并为 .a, .b, .c { ... } 单条规则。南瓜大作战 H5 实测合并 209 条。

Pass 3.5 — 重复元素抽取

Pass 2 合并了 CSS,但 HTML 里依然写了 N 个不同的 hash 类.prop__68 / .prop__105 / ...)。RepeatClassUnifier 进一步:≥ 3 个 .<base>__<digits> 形式的等价类 → 合并为单一 base 类(.prop),HTML 同步改写。

最终 HTML 里你看到的就是:

<div class="prop-list v-list">
  <div class="prop layer-group">...</div>
  <div class="prop layer-group">...</div>
  <div class="prop layer-group">...</div>
</div>

直接就是这种干净的语义化结构。

6.7 实战效果(南瓜大作战 H5)

指标 V2 优化器 当前版本
元素位置偏移 PSD 原位置 94 个元素偏离 5~13px 0 个元素偏离
CSS 行数 4805 1499
CSS 块数 457 ~270
z-index 字段 432 97
6×4 任务网格识别 每个 cell 独立 absolute 自动识别 v-col + v-row 嵌套

下面这张是真实产物里"任务格子"那段——20 多个图层、4 行多列、每个 cell 带描边小图标,全部由算法自动识别:

pumpkin-grid.png

6.8 算法的天花板与人工边界

再好的算法也有上限——下面这些场景 psd2code 会"尽力而为,但结果不一定最优":

① Flex 布局化不充分:设计师图层组织混乱

典型问题:活动页版块 2 的按钮、图标、装饰全部散乱摆在同一个 PSD 根组,没有任何分组——聚类算法能看到的只是 bbox 位置,看不到"设计意图"

👉 解决方案:整理 PSD 图层结构。按视觉版块分组(版块1-签到 / 版块2-道具 / 版块3-任务),每个版块内部再按"标题 / 卡片列表 / 底部按钮"分组。psd2code 会优先在已有组内部做聚类,组边界 = 聚类边界。分好组之后,95% 的场景都能自动重构为干净的 Flex。

② CSS 不够语义化:图层名用了默认命名

典型问题:PSD 图层名是 矢量智能对象图层 12 拷贝 3形状 47——psd2code 只能给你 .img-a3f012 这种内容哈希名,无从推断语义。

👉 解决方案:整理图层命名。重要的结构性图层给中文或 kebab-case 命名(bg-main / btn-领取 / 用户信息背景 / 任务卡片)。psd2code 的 semantic/ 子包能识别:

  • 按钮语义:btn / 按钮 / 领取 / 确定.btn-receive / .btn-ok
  • 背景语义:bg / 背景 / 底框.bg-main
  • 卡片容器:prop / card / 道具.prop-card
  • 中文关键词:通过 common/cn_dict.json 词典映射到 kebab-token

命名整洁之后,HTML 就会是 .prop-card / .btn-receive / .user-info-bg 这种一眼看懂的语义类,而不是 hash 串。

③ 人工干预:特殊场景需人工调整

psd2code 只实现常用渲染器所以部分图层导出效果不好(全实现产出比太低),需要人工干预。

👉 解决方案:手动栅格化或导出图片


七、实战演练:把"南瓜大作战 H5"PSD 跑一遍

南瓜大作战 H5(750 × 6778 长图活动页)为例,一行命令 20 秒拿到完整可运行 HTML:

$ python3 psd_to_code.py "南瓜大作战 H5.psd" --target html

🎨 合并背景图层: ['背景', '矩形 1', '形状 839 拷贝 2']
🖼️  background [合并3层 750x6778] → images/background-f07984.png
📁 solgan (组)
  ✨ 形状 16 (含效果渲染)
  🌟 检测到效果溢出 6px,使用混合渲染策略
📁 版块1 (组) ...
🎨 开始布局优化...
✅ 优化完成!
   - DOM 重构: 60 个
   - v-list 创建: 3 个 (包裹 24 个节点)
   - 应用 flex: 28 个
   - z-index 精简: 304 处
   - CSS 等价规则合并: 节省 128 条
   - 重复元素抽取: 25 组 → 删除 49 个 hash 类、复用到 61 个元素
   - 图层扁平化: 47 个容器 (共合并 105 层, 节省 45.6 KB)
✅ 产物:output/南瓜大作战 H5/html/

浏览器打开 index.html 第一屏——和 PSD 设计稿完全像素对齐,包括 solgan 上的描边发光、用户信息区的圆角、糖果图标的渐变叠加:

pumpkin-hero.png

absolute 原版 vs Flex 优化版对比

pumpkin-compare.png

文件 HTML 大小 CSS 大小 定位方式 可维护性
index.html 71 KB 113 KB 全部 position: absolute ⭐⭐
index_optimized.html 52 KB 38 KB Flex 嵌套 + 局部 absolute ⭐⭐⭐⭐

不要小看这 75 KB 的 CSS 压缩——它代表着 60 个容器被语义化、25 组 hash 类被复用,后期改样式不再需要逐个调 top/left

整个活动页 6778px 长,包含 9 大版块(用户信息 / 任务区 / 道具 / 助力 / 排行 等):

pumpkin-full.png

转换日志里有几个有意思的点:

  • 组级效果溢出自动触发 3 次:solgan 日期组(6px)、副标题组(10px)、糖果数目组(4px)——全部走"手动栅格化 + composite 混合"。
  • 47 个容器被图层扁平化:原本 105 张 image 合并成 47 张 PNG,节省 45.6 KB。
  • 3 组同质兄弟列表识别:道具卡 × 6、任务卡 × 12、排行榜条目 × 6,被包成 v-list——可直接写 v-for
  • 叠图组识别:邀请助力 / 核销助力码 / 版块3(7 个图层)等被 V8/V9 闸门正确识别为"装饰堆叠",保持 absolute。

八、多 Target 可插拔架构

8.1 Target Registry:装饰器注册

targets/registry.py 非常简单:

_REGISTRY: dict[str, Type[Target]] = {}

def register(name: str):
    def _wrap(cls: Type[Target]) -> Type[Target]:
        key = name.strip().lower()
        if key in _REGISTRY and _REGISTRY[key] is not cls:
            raise ValueError(f"Target '{key}' already registered")
        cls.name = key
        _REGISTRY[key] = cls
        return cls
    return _wrap

每个 target 是 Target 子类,实现 build_pipeline(ctx) -> Pipeline

# targets/html/target.py
@register("html")
class HtmlTarget(Target):
    def build_pipeline(self, ctx):
        return Pipeline([
            LoadPsdStage(),
            ParseStage(),
            ExtractAssetsStage(),
            CodegenStage(),
            LayoutOptimizeStage(),
            EmitStage(),
        ])

# targets/react/target.py
@register("react")
class ReactTarget(Target):
    def build_pipeline(self, ctx):
        return Pipeline([
            HtmlTarget().build_pipeline(ctx),   # 先产出 HTML(含优化版)
            Html2ReactStage(),                  # 再转 JSX + Vite 脚手架
        ])

# targets/vue/target.py 同理

CLI --target vueget("vue") → 实例化 → target.run(ctx)。新增 target(比如小程序)只需:

@register("mini-program")
class MiniProgramTarget(Target):
    def build_pipeline(self, ctx):
        return Pipeline([
            HtmlTarget().build_pipeline(ctx),
            Html2WxmlStage(),        # 转 WXML
            Html2WxssStage(),        # 转 WXSS
        ])

无需改动核心代码。

8.2 为什么 React / Vue 都在 HTML 基础上二次加工

业界也有"直接从 IR 生成 JSX"的设计,但 psd2code 选择"先走一遍 HTML target,再转框架":

  • HTML target 的优化(布局、CSS 去冗余、语义化命名)免费继承给 React/Vue——任何一次 LayoutOptimizer 升级自动惠及三端。
  • 开发者本地 review 时可以直接对比 html/index_optimized.htmlreact/src/App.jsx 的视觉一致性,容易定位转换问题。
  • React/Vue 的转换就是 DOM 遍历 + class/style 重映射 + 模板语法替换,逻辑简单、可测试性强。

8.3 产物结构一览

output/<psd_stem>/
├── html/                       # 任何 target 都会先产出
│   ├── index.html              # absolute 版(与 PSD 像素级对齐)
│   ├── index_optimized.html    # Flex 优化版
│   ├── style.css / style_optimized.css
│   ├── main.js                 # 国际化等运行时逻辑
│   ├── metadata.json           # 图层树元数据
│   ├── class_alias_map.json    # 老 hash 类 → 新语义类的映射
│   └── images/                 # 切图 / 合成图 / 背景图
├── react/                      # --target react 产出
│   ├── package.json / vite.config.js
│   └── src/App.jsx, App.css, main.jsx, assets/images/
└── vue/                        # --target vue 产出
    ├── package.json / vite.config.js
    └── src/App.vue, main.js, assets/images/

React / Vue 产物开箱即用:

cd output/<psd_stem>/react   # 或 vue
npm install && npm run dev   # http://localhost:5173

九、其他你可能在意的细节

  • 图片去重:按内容 MD5,同一张装饰图只导出一次。
  • 语义化类名:图层名 预测四强.yucesi__152(拼音兜底),或通过 cn_dict.json 词典映射为 .predict-top4
  • 国际化预留:所有文本节点自动带 data-i18n-key,可通过 JS 动态替换。
  • 旋转/倾斜文本:自动降级为图片,保证视觉一致。
  • 剪贴蒙版:按 layer.clip 标志识别,合并成父图基底 + 描边/发光效果。

十、踩过的坑(写给后来者)

如果你打算自己实现 PSD → 代码工具,以下几个坑可以省你几天:

  1. transform.scale 不能忘——所有 FontSize 都要乘以 transform.a / transform.d
  2. shape + 图层样式描边 psd-tools 会整片涂描边色——必须手动用 SoCo + origination 合成基础图、给 alpha 加 padding 再描边。
  3. 两层 enabled 开关必须 AND——layer.effects.enabled(整体)和 effect.enabled(单项)都要为 True 才算生效。
  4. composite() 的 viewport 限制——任何层级的 composite 都按"目标节点 + 所有祖先"的 bbox 硬裁切,不存在绕过方案,组级溢出效果必须手动扩展画布。
  5. 子组必须用 composite 渲染——不要退回手动递归,会在圆角处多出 ~75px/行的错误描边。
  6. tree.children 顺序 ≠ z 序——背景剥离后再合并 background-image 时,必须按原 DOM sibling index 重排。
  7. 多 url 背景合成时 reverse 列表——CSS 第一个 url 是视觉最上层,但 PIL alpha_composite 期望底层在前,反了会颜色对调。
  8. CSS parser 别用贪婪正则——@media (...) { #canvas { ... } } 嵌套时,简单正则会把内层 #canvas 误当顶层规则,整个 canvas 塌成 0 高。
  9. flex 容器 envelope 越界envelope.left/top 可能为负(图层超出组 bbox),写 padding 时要 max(0, ...),否则 cross_offset 算多了。
  10. v-stack wrapper 的 position 必须保留——flex_applier 默认 del child_css['position'],遇到 v-stack 要改写为 relative,否则内部 absolute 子元素会跳到外层定位。
  11. background-repeat: no-repeat 不是默认值——background-repeat 的 CSS 默认值是 repeat,CssDedup 删默认值时不能把它加进去,否则大背景图会被平铺。

十一、写在最后

psd2code 不是一个"AI 读图猜布局"的玩具——它是一个严格基于 PSD 结构信息的编译器。每一步决策都可解释、可调参、可单测,算法失败点(比如 V8/V9/V10 闸门)都有明确的 fallback 路径。

再强调一次:算法做的再多效果也是有限的。想要 psd2code 产出高质量代码,有两件事你得做:

  1. 整理 PSD 图层结构(按视觉版块分组)
  2. 整理 PSD 图层命名(关键图层给语义名)

做到这两点,运营活动页从设计稿到可上线代码的时间可以从 4~6 小时降到 20 秒。

未来计划:

  • ✅ HTML / React / Vue 已上线
  • 🚧 小程序 target(架构已预留扩展点)
  • 🚧 Tailwind CSS 输出
  • 🚧 Figma 文件支持(共享 IR,新增 figma loader)

如果你也在做活动页 / 长图详情页 / 运营 H5,欢迎试用并提反馈。


Thanks

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。
谢谢~~

源代码地址

前端开发者做 Agent:别只会执行,用 4 类失败策略让 AI 知道怎么停

2026年5月3日 08:44

作者:前端转 AI 深度实践者

【省流助手/核心观点】:Agent 做 demo 时,最显眼的是“它会调用工具”。但进入真实工程后,更关键的是“工具失败时它怎么办”。可靠的 Agent 必须区分错误类型:超时可以重试,空结果可以降级,权限不足要停止,高风险操作要等待确认。Agent 的成熟度,不取决于它能跑多远,而取决于它在出错时有没有刹车、有没有解释、有没有边界。


前面几篇,我们已经把 Agent 的核心骨架搭起来了。

  • 第 23 篇:Tool Calling,让模型学会调用工具。
  • 第 24 篇:工具 Schema,让工具有参数规则和风险边界。
  • 第 25 篇:Agent Loop,让模型和工具形成闭环。
  • 第 26 篇:Plan-Act-Observe,让 Agent 能处理多步任务。

这一路下来,Agent 看起来越来越像一个能做事的系统。

但还有一个问题必须正面面对:

如果某一步失败了,Agent 该怎么办?

这个问题比“怎么调用工具”更接近真实工程。

因为 demo 里的工具通常都成功。
真实世界里的工具不一定。

1. 痛点:会执行不难,失败后不乱跑才难

想象一个任务:

帮我查一下订单 A1001 的物流,如果还没送达,再查延迟补偿政策。

一个理想流程是:

查订单
-> 判断是否送达
-> 查政策
-> 给出建议

但真实执行时可能发生很多事:

  • 订单接口超时。
  • 订单不存在。
  • 用户没有权限查这个订单。
  • 政策搜索没有结果。
  • 工具参数缺失。
  • 高风险工具需要用户确认。

如果 Agent 没有失败策略,它可能会做出很糟糕的行为:

  • 接口超时后直接放弃。
  • 订单不存在还继续查政策。
  • 权限不足却假装查到了。
  • 政策为空却编一个政策。
  • 连续重试同一步,卡到超时。

所以 Agent 真正难的不是“能不能执行”,而是失败时知道该怎么办。

2. 错误做法:把所有失败都当成普通异常

很多初版系统喜欢这样返回:

type BadToolResult = {
  ok: false;
  error: string;
};

比如:

{
  "ok": false,
  "error": "执行失败"
}

这当然比程序直接崩掉好,但对 Agent 没什么帮助。

因为它不知道下一步应该做什么。

更危险的是,很多系统会写出这种“无脑继续”的逻辑:

async function unsafeContinue(plan: PlanStep[]) {
  for (const step of plan) {
    const result = await act(step);
    step.observation = result;
  }

  return generateFinalAnswer(plan);
}

这段代码的问题是:不管工具成功还是失败,后续步骤都继续执行。

如果查订单失败了,还继续查补偿政策;如果权限不足,还继续生成完整答案。用户看到的结果很顺,但可信度已经坏了。

3. 正确做法:给失败分类,再映射处理策略

先把错误类型结构化。

type ToolErrorType =
  | "invalid_arguments"
  | "unknown_tool"
  | "not_found"
  | "timeout"
  | "permission_denied"
  | "confirmation_required"
  | "empty_result";

type ToolResult =
  | {
      ok: true;
      data: unknown;
    }
  | {
      ok: false;
      errorType: ToolErrorType;
      message: string;
    };

每一种错误的含义都不同:

  • timeout:可能只是网络抖动。
  • invalid_arguments:模型或程序传参错了。
  • not_found:目标资源不存在。
  • permission_denied:用户没有权限。
  • confirmation_required:动作有风险,需要用户确认。
  • empty_result:查询成功了,但没有找到内容。

错误类型越清楚,Agent 越能做正确决策。

接着设计处理策略:

type FailureDecision =
  | "retry"
  | "fallback"
  | "pause"
  | "stop";

const failureStrategies: Record<ToolErrorType, FailureDecision> = {
  timeout: "retry",
  empty_result: "fallback",
  not_found: "stop",
  invalid_arguments: "stop",
  unknown_tool: "stop",
  permission_denied: "stop",
  confirmation_required: "pause"
};

这张表非常朴素,但非常有用。

它把“失败了怎么办”从一句模糊的话,变成了明确工程策略。

4. 重试要克制,不要把 retry 当万能药

看到失败,很多人的第一反应是:

那就重试。

重试确实有用,但不能滥用。

比如接口超时,可以重试。
但下面这些错误,重试通常没有意义:

  • 工具不存在。
  • 参数缺失。
  • 用户没权限。
  • 订单不存在。

所以每个步骤都应该有重试计数:

type PlanStep = {
  id: string;
  goal: string;
  toolName: string;
  args: Record<string, unknown>;
  status:
    | "pending"
    | "running"
    | "done"
    | "failed"
    | "paused"
    | "fallback";
  retryCount: number;
  maxRetries: number;
  observation?: unknown;
  error?: ToolResult;
  fallbackReason?: string;
};

重试判断可以这样写:

function canRetry(step: PlanStep, result: ToolResult) {
  return (
    !result.ok &&
    result.errorType === "timeout" &&
    step.retryCount < step.maxRetries
  );
}

没有上限的重试,不叫韧性,叫迷路。

5. 降级不是糊弄用户,而是诚实表达边界

还有一种失败很常见:空结果。

比如政策搜索工具返回:

{
  "ok": false,
  "errorType": "empty_result",
  "message": "没有找到相关政策"
}

这时候不一定要让整个任务失败。

Agent 可以降级回答:

我查到了订单 A1001 当前仍在运输中,但没有找到明确的延迟补偿政策。建议你联系人工客服确认是否可申请补偿。

这就是降级。

降级不是假装成功。

降级是:

  • 告诉用户哪些信息查到了。
  • 告诉用户哪些信息没查到。
  • 不编造不存在的依据。
  • 给出下一步建议。

这比“为了完整而胡编”可靠得多。

6. 暂停也是一种能力

有些操作不能失败后直接结束,也不能自动继续。

例如:

帮我直接取消这个订单。

取消订单是有副作用的高风险操作。

即使模型判断要调用 cancelOrder,程序也应该返回:

{
  "ok": false,
  "errorType": "confirmation_required",
  "message": "取消订单需要用户确认。"
}

这时候 Agent 的正确行为不是继续执行,而是暂停:

这个操作会取消订单 A1001。请确认是否继续。

暂停不是不智能。

暂停是安全边界的一部分。

一个系统如果不知道什么时候停下来问用户,就不适合处理真实业务。

7. 把失败处理接进 Plan-Act-Observe

第 26 篇我们有:

Plan -> Act -> Observe

现在加上失败策略:

Plan
-> Act
-> 如果成功:Observe Success
-> 如果失败:Handle Failure
-> Retry / Fallback / Pause / Stop

核心代码可以这样写:

function handleStepFailure(step: PlanStep, result: ToolResult) {
  if (result.ok) return "none";

  const decision = failureStrategies[result.errorType];

  if (decision === "retry" && canRetry(step, result)) {
    step.retryCount += 1;
    step.status = "pending";
    step.error = result;
    return "retry";
  }

  if (decision === "fallback") {
    step.status = "fallback";
    step.fallbackReason = result.message;
    step.error = result;
    return "fallback";
  }

  if (decision === "pause") {
    step.status = "paused";
    step.error = result;
    return "pause";
  }

  step.status = "failed";
  step.error = result;
  return "stop";
}

再把它接进执行循环:

async function runStepWithFailureControl(step: PlanStep) {
  step.status = "running";

  const result = await act(step);

  if (result.ok) {
    step.status = "done";
    step.observation = result.data;
    return "continue";
  }

  const decision = handleStepFailure(step, result);

  if (decision === "retry") {
    return "retry";
  }

  if (decision === "fallback") {
    return "continue";
  }

  if (decision === "pause") {
    return "pause";
  }

  return "stop";
}

这让 Agent 多了一套刹车系统。

它不再是“计划里有几步就硬跑几步”,而是会根据错误类型做不同处理。

8. 最终答案必须解释失败

失败处理还有一个关键点:最终答案不能只说“失败了”。

它应该说明:

  • 哪些步骤成功了。
  • 哪些步骤失败了。
  • 失败原因是什么。
  • 是否重试过。
  • 是否降级了。
  • 用户下一步可以做什么。

可以从 plan 里生成一个更清楚的回答:

function summarizePlan(plan: PlanStep[]) {
  const done = plan.filter((step) => step.status === "done");
  const failed = plan.filter((step) => step.status === "failed");
  const fallback = plan.filter((step) => step.status === "fallback");
  const paused = plan.filter((step) => step.status === "paused");

  return {
    done: done.map((step) => step.goal),
    failed: failed.map((step) => ({
      goal: step.goal,
      reason: step.error && !step.error.ok ? step.error.message : "未知错误"
    })),
    fallback: fallback.map((step) => ({
      goal: step.goal,
      reason: step.fallbackReason
    })),
    paused: paused.map((step) => ({
      goal: step.goal,
      reason: step.error && !step.error.ok ? step.error.message : "等待确认"
    }))
  };
}

用户侧表达可以是:

我查到了订单 A1001 当前仍在运输中。
但在查询延迟补偿政策时没有找到明确结果,因此不能确认是否可自动申请补偿。
建议你联系人工客服,并提供订单号 A1001 进一步确认。

这类回答虽然不“全能”,但可信。

AI 产品最怕的不是说“我不知道”。
最怕的是不知道还装知道。

9. 前端开发者怎么理解失败处理

前端其实非常懂失败处理。

你写页面时不会只写成功态。

你还会考虑:

  • loading。
  • empty。
  • error。
  • disabled。
  • retry。
  • permission denied。
  • confirm modal。

Agent 也是一样。

工具调用的失败态,就是 AI 系统里的 error state。

高风险确认,就是 AI 系统里的 confirm modal。

空结果降级,就是 AI 系统里的 empty state。

重试上限,就是 AI 系统里的防抖和保护阈值。

所以前端经验在这里非常有价值。

你不是从零开始学 Agent。

你是在把已有的工程直觉迁移到 AI 系统里。

10. 生产环境避坑指南

1. 只重试临时性错误

适合重试的通常是 timeout、临时网络错误、上游服务短暂不可用。

不适合重试的是 invalid_argumentspermission_deniednot_foundunknown_tool

2. 重试必须有上限和间隔

每一步都要有 retryCountmaxRetries

更进一步,可以加指数退避,避免把上游服务打爆。

3. fallback 不能伪装成成功

降级回答必须告诉用户哪些信息拿到了,哪些信息没拿到。

不要把空结果包装成确定结论。

4. pause 必须能恢复

如果高风险操作进入 paused,前端要能保存当前 plan,并在用户确认后从暂停步骤继续。

不要让用户确认之后系统重新从第一步跑一遍。

5. 失败要进日志和评测集

每次失败都应该记录:

  • traceId
  • step id
  • tool name
  • errorType
  • retryCount
  • final decision

高频失败样本应该回流到测试用例或 Agent 评测集。

11. 常见误区

误区 1:失败了就让模型再试一次

不对。模型重试不是万能药。只有临时性错误才适合重试。

误区 2:降级就是糊弄用户

不是。好的降级是透明说明边界,不编造结果,并给出下一步建议。

误区 3:工具失败了也让 Agent 继续跑完整计划

危险。关键步骤失败时应该停止或暂停,而不是继续生成看似完整的答案。

误区 4:错误信息写给开发者看就行

不够。内部错误要结构化,用户侧表达要清楚、克制、可行动。

12. 给前端开发者的落地清单

如果你在团队里做 Agent 失败处理,可以从这份清单开始:

  1. 所有工具失败都必须返回 errorType
  2. 错误类型要能映射到处理策略。
  3. 只有临时性错误才允许重试。
  4. 每一步都要有 retryCountmaxRetries
  5. 高风险工具必须支持暂停确认。
  6. 空结果可以降级,但不能假装成功。
  7. 关键步骤失败后不要继续硬跑。
  8. 最终答案要说明成功、失败、跳过和下一步建议。
  9. 执行日志要记录每次重试。
  10. 测试用例必须覆盖成功、重试、降级、暂停、停止。

这份清单不华丽,但很保命。

Agent 越像能办事的系统,越要认真处理失败。

结语

Agent 真正难的,不是会执行。

真正难的是失败后不乱执行。

它要知道什么时候重试,什么时候降级,什么时候暂停,什么时候停止。

这听起来没有“自主智能体”那么炫,但它决定了系统能不能进入真实业务。

一个可靠的 Agent,不是永远顺利地跑到终点。

一个可靠的 Agent,是遇到坑时不会假装没看见,而是踩刹车、留痕迹、说清楚,然后给用户一个可信的下一步。

HelloGitHub 第 121 期

2026年4月28日 08:05
本期共有 40 个项目,包含 C 项目 (1),C# 项目 (2),C++ 项目 (2),Go 项目 (4),JavaScript 项目 (4),Kotlin 项目 (1),Python 项目 (3),Rust 项目 (4),Skills (4),Swift 项目 (3),人工智能 (5),其它 (5),开源书籍 (2)

你写的 Skill,及格了吗?

作者 百度Geek说
2026年4月23日 10:14

导读 introduction

本文提出了一套8维度的Skill量化评估框架,通过元数据质量、执行引导清晰度、领域知识密度等指标对Skill进行打分评级,解决了Skill质量难以客观衡量的问题。为提升评估可靠性,设计了多模型交叉验证流程,并适配不同AI工具环境提供四种执行策略。该框架既能帮助开发者识别改进短板,也能辅助用户横向对比选择优质Skill,但需注意其侧重于文档与设计质量评估,并非运行时性能的完整度量。

00 为什么需要一把尺子?

图片

Skill 是 Agent 能力的最小封装单元,它把领域知识、工作流程和工具集成打包成一个即插即用的模块,让通用 Agent 秒变领域专家。

现在所有人都在写 Skill、分享 Skill,但面临的问题是:

  • 👷 你写的那个 Skill,真的够好吗?

  • 🛜 网络上获取的 Skill 选哪一个更好?

能跑好用之间隔着十万八千里。

一个 Skill,description 写得太宽泛了,很可能 Agent 根本不会触发它;工作流缺少分支逻辑,可能碰到稍复杂的输入就翻车;明明需要附带脚本却硬塞在 Markdown 里,每次执行都重写一遍相同的代码。更麻烦的是,这些问题写的时候不一定能看出来,只有真正使用的时候才会暴露(也有可能不会暴露)。

基于此设定了一套 8 个维度的量化评估框架并实现了一个评估 Skill,从元数据质量、执行引导、领域知识密度到工作流完整性等,逐项打分、加权汇总,最终给出 Skill 的 S/A/B/C/D 等级评定。它能帮你审视自己的 Skill:哪里还有短板、该怎么改;也能横向对比多个 Skill:谁设计得更好、好在哪里。

审视自己的作品,它是改进路线图;对比他人的作品,它是选型决策工具

01 八个维度,把"感觉"变成"分数"

图片

我们要评估一个 Skill 的好坏,不能只靠感觉,必须要有一套可量化的标准。

1.1 八个评估维度分布在 Skill 生命周期的三个阶段中

下面的 8 个维度分布在一个 Skill 从被发现到被执行完成的完整生命周期,分成三个评估阶段:

第一阶段:能不能被找到

一个 Skill 装好之后,Agent 在每次对话中只会读到它的 name 和 description。如果这几十个字写得不好,Skill 就根本不会被触发,后面写得再好也没用了。

D1. 元数据质量 评估的就是这个第一印象。description 是否精准地描述了功能?是否包含了用户可能使用的关键词?最好的 description 甚至会写明不该在什么场景触发,防止误触发。

这是唯一一个决定 Skill 生死的维度。其他维度影响的是"好不好用",D1 决定的是"有没有机会被用到"。

第二阶段:用起来顺不顺

Agent 决定触发后,会加载 SKILL.md 的完整内容。这时候考验的是:Agent 能不能顺畅地把任务执行完?

这一阶段有四个维度:

  • D2. 执行引导清晰度:Agent 加载了 Skill,但面对用户的具体输入,它知道该走哪条路吗?什么情况需要追问用户?什么情况该直接执行?什么操作不该做?好的 Skill 像一本清晰的操作手册,而不是一堆信息的堆砌;

  • D4. 工作流完整性:如果说 D2 是每一步怎么走,D4 就是整条路是否走得通。流程是否端到端?步骤之间的衔接是否顺畅?碰到异常(API 超时、文件下载失败)有没有处理方案?

  • D5. 输入输出清晰度:用户给什么、最终得到什么?这听起来是基本功,但很多 Skill 只写了中间步骤,用户第一次看完全不知道整个流程的起点和终点是什么;

  • D6. 资源利用:该用脚本的地方是不是用了脚本?该放参考文档的地方是不是放了?还是把所有东西都塞在一个巨大的 SKILL.md 里?好的资源结构遵循渐进式披露SKILL.md 保持精简,详细内容按需加载);

第三阶段:值不值得存在

最后,跳出执行细节,从更高的视角审视这个 Skill 本身。

  • D3. 领域知识密度:这是 Skill 存在的根本理由。如果 Skill 里的内容,通用 Agent 不靠它也能做到,那它就没必要存在。好的 Skill 内嵌了难以获取的专业知识:私有 API 的调用方式、内部系统的数据模型、行业特定的最佳实践。

  • D7. 写作质量SKILL.md 本质上是写给另一个 AI 看的技术文档。结构是否清晰?有没有冗余?Agent 能否快速扫读并抓到关键信息?

  • D8. 范围与聚焦:一个 Skill 应该做好一件事,而不是试图包揽一切。过宽的 Skill 什么都做不好,过窄的 Skill 不值得封装。

1.2 权重与评级

并非所有维度同等重要。我们按对 Skill 实际效果的影响程度分配权重:

图片

每个维度具体的评分方式这里不赘述,有兴趣的同学可以从附录下载 Skill 查看。

加权求和后,总分映射为五个等级:

图片

接下来,我们用这套框架实际评估一个 Skill,看看分数背后的含义。

02 拿自己开刀:一个真实的 Skill 评估

光看评分标准还是比较抽象,这里先拿一个真实的 Skill 跑一遍评估。

演示中使用的是百度内部 AI 工作助手 dodo。使用其他支持 Skill 的 AI 工具如 Claude Code、小龙虾等也都是可以的。

下面参与评估的这个 Skill 是我在内部另一篇实例中真实开发并已投入使用的:

图片

评估得出的具体问题与建议摘录如下:

问题与建议

D1. 元数据质量 (3/10):description 过于简略,缺少触发条件

  • 影响:Agent 无法自动识别何时应触发此 Skill

  • 建议:扩展 description,包含触发关键词和功能概述

D7. 写作质量 (7/10):存在重复内容

  • 影响:文档冗余,影响可读性

  • 建议:删除重复的 SQL 示例(第 333-347 行),保留一处即可;合并重复的说明段落

总体评价

ip-bill 是一个功能完整、执行清晰的 Skill,适合自动化处理公网 IP 出账流程。主要优势在于:

  • 工作流设计严谨,包含连通性检查和错误处理

  • 领域知识丰富,API、数据库、BOS 等各环节都有详细说明

  • 资源组织合理,脚本、参考文档、示例文件齐全

主要改进空间在于元数据描述文档精简。增强 description 中的触发条件描述,有助于 Agent 自动识别使用场景。

03 选谁好?两个同类 Skill 的正面对决

这里从 dodo 的可用 Skill 列表中随便选取了两个功能相近的 Skill 进行评估对比:

  • Skill A:workos-weekly

  • Skill B:subordinate-weekly-report

详细评估结果摘录如下:

图片

各自优劣势

workos-weekly 更强的方面:

  • 领域知识密度更高(OKR 关系、活跃空间、洞察分析等)

  • 资源利用更丰富(references、examples、CHANGELOG)

  • 工作流更复杂(9 步端到端流程)

subordinate-weekly-report 更强的方面:

  • 元数据质量更高(场景路由表更清晰)

  • 输入输出清晰度更高(每个 action 都有完整的输入输出示例)

  • 场景覆盖更全面(6 种 action 覆盖多种查询场景)

subordinate-weekly-report 更弱的方面:

  • 领域知识密度较低(专业概念较少)

  • 资源利用不足(参考资料较少)

经验总结

两个 Skill 做得好的地方:

  • 都有明确的触发条件和反向排除条件

  • 都有清晰的执行引导和错误处理

  • 都聚焦于单一场景,边界清晰

  • 写作质量都很高,结构清晰易读

可借鉴的经验:

  • workos-weekly 的 HARD-GATE 设计值得借鉴,能明确强制规则

  • subordinate-weekly-report 的场景路由表设计优秀,能快速匹配用户意图

  • 两个 Skill 都有详细的命令示例和输出示例,降低了 Agent 的理解成本

04 一个模型就够了?多模型交叉验证

图片

前面的评估案例中,分数评定由一个模型完成。这就像一场考试只有一个阅卷老师,他可能偏严,也可能偏松,我们却无从判断。

实测中很明显的看到,不同模型对同一个 Skill 的评分存在些许差异。例如用 GLM-5.1 评估某 Skill,得到 7.8 / A,换成 Claude Opus 4.6 评估则为 6.5 / B。分数差了一个等级,但两个模型指出的核心问题却是趋同的。

这说明单模型评分的绝对值不够可靠,但不同模型之间的共识是有价值的。

基于这个观察,我又给本 skill-evaluator 增加了多模型交叉验证机制:让多个模型独立评估、互相质疑、最终再由主模型完成仲裁,把一家之言升级为专家评审团。

4.1 评估流程

图片

第一阶段:独立评估

多个模型各自按 8 维度标准独立打分。

第二阶段:交叉互审

所有独立评估完成后,每个模型会看到其他模型的评分。它们需要找出与自己分差 ≥ 2 分的维度,明确指出对方哪里评得不合理,并且必须引用 Skill 中的具体内容作为证据。例如:你 D3 给了 8 分,但这个 Skill 的领域知识只覆盖了 API 调用方式,缺少数据模型和决策逻辑的描述(见第 45-60 行),7 分以上要求要有丰富的专家知识,我认为 6 分更合理。

并且,在审查完对方之后,每个模型还要做自我修正:看了对方的论据后,我是否要调整自己的分数?这一步迫使模型认真对待对方的质疑,而不是固执己见。

第三阶段:仲裁综合

由主模型(仲裁者)汇总所有独立评估和交叉互审的结果,对每个维度做出最终裁决。

4.2 三级共识机制

针对于每个评估维度,这里设计了一个三级共识机制,用于主模型执行仲裁:

图片

在最终的评估报告中,每个维度都会标注共识度,让使用者能直接看出哪些分数是比较确定的,哪些是有争议的。

例如:

图片

如果一个维度被标记为"仲裁",说明这个维度的质量确实处于模糊地带,值得 Skill 作者重点关注。

4.3 当多模型不被支持时

不是所有环境都支持多模型调用,为此在这里我设计了一个兜底方案:单模型多视角评估。

在这个方案中,单一模型扮演三个不同的评审角色,依据评审风格分为:

图片

三个视角评完后同样进行交叉审查和仲裁。

虽然本质上还是一个模型,但通过角色分化强制引入多样性:严格派可能在 D4 给 5 分,务实派同一维度给 8 分,这种分歧是有意义的。

05 四种执行策略的自动路由

不同的 AI 工具能力不同。有的工具(如 Claude Code)支持通过 subagent 同时调用自家多个模型,dodo 也支持这种模式调用 Claude 的 Opus 4.6、Sonnet 4.6、Haiku 4.5 模型。但如果想融合更多其他第三方模型或自定义评估模型列表目前还没有原生支持。所以这里在 Skill 中集成了千帆平台的模型能力,对于上述 Claude 系列之外的模型则通过千帆的能力支持。

5.1 四种策略

如果交叉验证只能在特定工具上跑,这个功能的适用范围就很窄。为了保证在各种 AI 工具及不同的触发场景中本 Skill 执行的兼容性,这里设计了四种执行策略:

图片

从使用者的视角,只需要提供参与评审的模型:指定主模型(仲裁模型)和交叉评审模型。

策略路由逻辑根据模型列表自动分类:

  • 全是工具原生模型走 A;

  • 全是第三方走 B,两者都有走 A + B;

  • 如果运行中出了问题自动降级到策略 C;

图片

策略 B 依赖一个 Python 脚本来调用千帆 API。这个脚本只使用了 Python 标准库,不需要操心依赖之类的问题。脚本支持了多模型并行调用和自动重试。

5.2 模型组合与实际效果

这里测试了几种模型组合的评审效果:

图片

当前 Skill 中已默认配置了模型,主模型为:当前 Agent 自身设置的模型;交叉互评模型为:ernie-5.0、sonnet、glm-5.1、minimax-m2.5。

06 三堂会审:多模型评估实践

上文中对我写的 ip-bill 这个 Skill 进行了单模型的评估,最终评估结果虽然有理有据,但是我们仍然无法确定其权威性,一家之言终归是片面的:这个模型的结果是偏严了还是偏松了?它指出的问题是最关键的还是恰好它注意到的?那些没被扣分的维度,是真的没问题,还是被这个模型忽略了?

用多模型交叉验证对 ip-bill 再评一次。这次由 ernie-5.0、sonnet 4.6、glm-5.1、minimax-m2.5 四个来自不同厂商的模型独立评估,互相质疑,最终由主模型 Claude Code Opus 4.6 仲裁出一份经过质证的结论。评估结果篇幅较长,这里我直接导出了一份报告文件:

图片

图片

图片

图片

图片

图片

图片

图片

07 评完之后怎么改?

拿到评估结果和改进建议后,可以继续让 AI 助手代劳。本文为了演示方便,拆分成了评估 + 优化,懒得看结果的话可以让 AI 助手直接一步处理分析 + 优化(这里演示的是单模型评估后优化,多模型的操作也是类似的)。

图片

更多的用法实践可以自行扩展,例如多 Skill 的对比优化,互相学习其优点改进缺点等等,主要还是要依赖模型的分析能力。

08 写在最后

回到开头的两个问题:自己写的 Skill 够不够好?网上下载的选哪个?

现在你有了一套 8 维度的评估框架来回答它们,给 Skill 打分、找短板、做对比,把感觉还行变成可量化的判断。并且可以选择通过多模型交叉评估来避免单模型一言堂的造成的评估局限,但是多模型也会增大 Token 的消耗和 Skill 的运行时间,具体使用哪种方式来评估可以由使用者自由选择。

随着更多人写 Skill、分享 Skill,我们可能需要更完善的评估手段:自动化的静态检查工具、基于实际执行数据的动态评分、社区驱动的 Skill 评级体系。这些都值得探索。

这套评估框架只是一把尺子,也有一定的局限性,它度量的是 Skill 的文档工程质量,而非运行时的全部真相。

知道它能量什么、不能量什么,才能用好它。

09 附录

本案例中的 Skill 地址

github.com/sunxingboo/…

我把 Karpathy 的 AutoResearch 搬到了软件开发领域,效果炸了

作者 百度Geek说
2026年4月21日 16:01

导读

本项目成功将Karpathy在AI研究领域的AutoResearch方法迁移到软件开发领域,通过多AI Agent交叉审核、5维度量化评分和反馈驱动迭代三大改进,构建了一个全自动的软件开发系统。该系统以program.md为规则核心,实现从GitHub Issue识别、代码实现、测试验证到审核合并的完整闭环,仅在少数情况下需要人工介入。实践表明,该系统能在约10分钟内自主完成中等复杂度的开发任务,并达到9.0/10的代码质量标准,显著提升了开发效率并降低了人力成本。

像 Karpathy 训模型一样开发软件。

图片

1 项目介绍

项目地址:

github.com/smallnest/a…

最近做了优化:

  • 将此工具抽取成独立的项目

  • 代码进行了重构,增加了更多的控制

  • 通用化, 可以应用于任意的github项目

  • 增加了opencode,可以实现1个到3个任意组合的Coding Agent交叉审核和代码实现

图片

2 什么是 Karpathy AutoResearch?

2026 年 3 月,AI 领域知名研究者 Andrej Karpathy 发布了 autoresearch 项目,短短几天内就在 GitHub 收获 5 万+ 星标,Karpathy 发布的介绍视频播放量达 860 万次。这是一款开源 Python 工具,代码量约 600 行。

核心思想是:把 AI 研究本身也交给 AI 来自主完成。

具体做法极简而优雅:给 AI Agent 一个真实的小型 LLM 训练环境(单 GPU,5 分钟训练预算),让它自主修改 train.py、跑实验、检查结果——只有 val loss(验证集损失)改善时才 commit,否则 git revert 回滚,然后继续下一轮。人类只需维护一份 program.md(相当于给 Agent 的「研究章程」),剩下的全部交给 Agent 晚上自己跑。

这个项目的精髓在于三点:① 量化目标(val loss 是唯一判断标准)、② 自主循环(Agent 不需要人类每轮介入)、③ 只保留改进(退化就回滚,绝不将就)。预计每小时可完成约 12 次实验,一觉醒来就能收获上百轮自动优化的结果。

Andrej Karpathy的这套思路在 ML 研究领域验证有效后,我开始思考:软件开发领域能否复刻同样的魔法? 把"修改 train.py → 跑 5 分钟实验 → val loss 改善才保留",替换成"实现 GitHub Issue → 跑测试 → 多维评分达标才合并"——这就是本项目的起点。实测下来,10 分钟完成一个中等复杂 Issue,全程零人工干预,最终评分 9.0/10。

Issue#21自动化实现的回放地址: 

asciinema.org/a/896260

这个回放解决的Issue#21: 

github.com/smallnest/i…

前几天正好看到花叔的写的一个SKill:达尔文.skill, 殊途同归—— 他在Skill开发 领域同样应用AutoResearch方法实现对Skill技能的优化。后来花叔把这个经验总结到他的另外一个Skill项目上:auto-optimize-skill。

图片

3 为什么做这个?

传统的"人类写代码 → 运行测试 → 修复问题"流程,在 GitHub Issues 有几十上百个待处理项时不再可行。

即使用 Claude Code / Codex 等 AI 编程工具(所谓的 vibe coding),你仍然需要:

  • 一轮一轮地 chat 交互,告诉 AI 做什么

  • 人工检查输出、发现问题、再告诉 AI 改什么

  • 生成的代码是一堆『屎山💩

  • 人始终被绑在循环里,离开就不转了

2025 年底流行的 Ralph Wiggum 方法(while true; do cat PROMPT.md | claude; done)更进一步:写好 SPEC,让单 Agent 在循环里自主干活。解决了人的 chat 交互问题,但本质是单个 Agent 的自我循环——自己写、自己测、自己改,没有外部审核视角,质量全靠测试 backpressure 和 prompt 工夫。

2026 年 3 月 Karpathy 发布了 autoresearch,把同样的循环思路用到了 ML 研究领域:写一个 program.md 定义目标和约束,AI 自主修改训练代码、跑 5 分钟快速实验,只有 val loss 改善时才 commit,否则 git revert。核心创新是把"什么是改进"量化成了一个明确的 metric。

本项目的 Autoresearch 在 Karpathy 思想基础上做了三个关键改进:

1. 多 Agent 交叉审核,替代单 Agent 自审。Ralph Wiggum 和 Karpathy AutoResearch 都是单 Agent 自己改自己评,缺少外部视角。本项目让 Codex 和 Claude 轮流担任实现者和审核者:A 写完 B 审,B 写完 A 审。不同模型有不同的盲区和强项,交叉审核能发现单 Agent 发现不了的问题。实践证明,单 Agent 的效果远不如双 Agent 交叉审核。本项目创造性地使用两个 Agent 轮流审核和开发,极大地提高了代码质量。

2. 5 维度加权评分,替代单一 metric。 Karpathy 用 val loss 一个数字判断好坏,ML 场景足够用。但软件工程的质量是多维的——功能正确、测试充分、代码规范、安全无漏洞、性能没坑。本项目用 5 维度加权评分(正确性 35% + 测试 25% + 代码质量 20% + 安全 10% + 性能 10%),总分 ≥ 9.0 才算通过,把"代码好不好"从主观判断变成量化指标。

3. 审核反馈驱动下一轮实现,替代盲循环。 Ralph Wiggum 的每轮循环是独立的——新上下文重新开始,不记得上轮犯了什么错。本项目的审核反馈直接传入下一轮 Agent 的提示词,Agent 看到上一轮的具体问题后针对性改进,而不是漫无目的地重试。

最终效果:人只提供 Issue 号,剩下的全自动——自动实现、自动测试、自动审核、自动迭代、评分达标后自动 PR + 合并。

图片

图片

与同类项目对比

图片

本节对比三个将"自主迭代循环"思想应用到不同领域的项目:Karpathy 的 AutoResearch 用于 ML 研究,本项目用于通用软件开发,达尔文.skill 用于 Skill 优化。三者核心机制相同——量化目标 + 自动迭代 + 只保留改进——但在被优化的资产、质量保证机制、人的参与程度等方面做出了不同选择。

图片

从对比可以看出:

  • 量化目标是共通的核心。三个项目都把"什么是改进"定义成了可量化的指标——val loss、审核评分、8 维总分——而不是依赖人的主观判断。

  • 质量保证机制各有侧重。Karpathy 和达尔文.skill 用 git revert 做硬性保护(退化就回滚),本项目用多 Agent 交叉审核做软性保护(审核反馈驱动改进,并没有做回退机制,原因在于ClaudeCode/Codex自己足够智能决定回退还是改进上一轮的变动)。

  • 人的参与程度反映了领域特征。ML 研究的 metric 足够客观,可以全自主;Skill 的好坏需要人的判断,所以每轮暂停确认;软件开发介于两者之间,大部分自动但保留关键节点介入能力。

4 系统架构

以下是这个项目的架构图:

图片

4.1 六条核心原则

图片

这六条原则是整个系统的设计基石。原则 01 定义了规则的来源和边界,原则 02-05 构成了多 Agent 对抗的质量保证链(谁来做、怎么评、怎么改进),原则 06 确保整个过程可追溯。它们相互配合:没有 program.md 的约束,Agent 会越权;没有多 Agent 对抗,单 Agent 自审会有盲区;没有量化门槛,质量判断就回到主观经验;没有反馈驱动,迭代就是盲循环;没有全量记录,出了问题无法回溯。

图片

4.2 审核评分体系

图片

审核评分是 AutoResearch 的量化核心——它把"这段代码好不好"从一个模糊的主观判断,变成一个 5 维度加权计算出的精确分数。这个分数决定了迭代是继续还是停止:≥ 9.0 自动提交 PR,< 9.0 审核反馈驱动下一轮改进。维度和权重的分配反映了软件工程的质量优先级:功能正确最重要(35%),测试其次(25%),代码质量(20%),安全和性能各占 10%。

总分 10 分,5 维度加权:

图片

各维度得分:无问题 10 分 / 建议改进 9 分 / 一般问题 7 分 / 严重问题 4 分 / 致命问题 1 分

达标线:9.0/10

4.3 优化循环:4 个阶段

图片

整个流程分为 4 个阶段。

  1. Phase 1 做环境准备(一次性,几秒钟)。

  2. Phase 2 是核心迭代循环——多 Agent 轮流审核和实现,测试验证,评分判定,这个阶段完全自主运行,不需要人介入。

  3. Phase 3 在评分达标后自动触发,完成 commit + PR + 合并。

  4. Phase 4 做结果归档,把迭代过程写入日志供回溯。其中 Phase 2 占了几乎全部时间,也是系统价值的核心所在。

    Phase 1: 环境准备

迭代示例:

迭代 1: Codex 审核  Codex 实现  测试  Claude 审核(5.0)  Claude 实现

终止条件:在以下情况下,任务会终止

图片

4.4 核心文件

autoresearch/

图片

4.5 Issue 选择策略

图片

排除规则:以下 Issue 不处理:wontfix / duplicate / invalid / blocked / needs discussion / on hold / external,标题含 [WIP]``[DRAFT],正文含 DO NOT IMPLEMENT,已有 PR 关联。

优先级计算:

分数 = 基础权重(15) + 标签权重 + 类型权重 + 时间因子
  • 标签权重:critical(100) > high(50) > medium(20) > low(10)

  • 类型权重:bug(30) > feature(20) > refactor(10) > test(5) > docs(3)

  • 时间因子:新 Issue +10 / 陈年 Issue +15 / 近期更新 +5

复杂度评估:

图片

4.6 program.md 要点

权限边界:

Agent 可以:

代码规范(Go):

1. 遵循 Effective Go + Go Code Review Comments

测试规范:

1. 所有新功能必须有单元测试

4.7 错误处理

图片

退火重试: API 调用失败时使用指数退避 + 随机抖动(delay = 2^retry * base_delay + random_jitter,最大等待 60 秒,最多重试 10 次)。

连续失败保护: Agent 执行失败 → 连续失败计数 +1,连续失败 ≥ 3 次 → 停止运行,记录日志。

测试失败: 测试失败 → 反馈"测试失败" → 下一轮 Agent 针对性修复。

4.8 运行结果

results.tsv 格式:

timestamp   issue_number  issue_title  status     iterations  tests_passed  score  branch_name

状态定义:

图片

5 快速开始

5.1 前置条件

因为需要自动化处理 GitHub 的 Issue,所以需要安装 GitHub CLI。

因为通过 acpx 操控 Claude Code 和 Codex,所以需要安装 acpx 工具。

因为本项目使用 Go 语言开发,所以需要安装 Go 环境。

# GitHub CLI (gh)

5.2 运行

调用run.sh脚本,直接输入issue号即可运行。

# 进入你要处理的 GitHub 项目目录

脚本会自动:检查环境 → 获取 Issue → 创建分支 → 轮流 Codex/Claude 实现+审核 → 达标后自动 PR + 合并

5.3 自定义配置

在项目根目录创建 .autoresearch/ 目录可覆盖默认配置:

.autoresearch/

6 实战案例

以下是我实际开发真实案例,特别的是 Issue #21, 我专门使用 asciinema 工具记录了这个issue自动开发的全过程。

Issue #21: feat: enhance job execution with agent selection and timeout

我只需提供一个Issue号,剩下的就由 autoresearch 脚本自动完成。

./docs/autoresearch/run.sh 21

默认设置最多执行 42 轮迭代,但通常几轮之后代码质量便能达到标准。下面是 Issue #21 的迭代过程,大约 10 分钟就完成了开发,总共迭代了 3 轮。

你可以点击这个回放链接 查看完整过程:

(回放链接:

asciinema.org/a/896260)

图片

关键日志:

复杂度:中等(涉及 Job 结构体扩展、超时控制、API 增强)

Issue #15: feat: define source-of-truth event protocol

实现 Issue #15 时,仅迭代两轮代码质量便达到了标准,关键日志如下:

迭代 1 (Codex):  评分 5.0  → 反馈:设计方向问题

Issue #6: feat: add web UI for sessions

实现 Issue #6 的时候关键日志,就迭代了5轮代码质量就达到了标准:

复杂度:高(涉及多个模块、需要设计决策)

7 最佳实践

图片

  1. 从小 Issue 开始:先用简单的 Issue (bug fix) 测试流程

  2. 保持 program.md 更新:根据运行情况调整规则和约束。一旦你在使用中觉得效果不够理想,比如评分机制不符合预期,就可以修改这个文件。

  3. 关注评分趋势:每次迭代的评分记录在 log.md 中,观察是否稳步上升

  4. 利用多 Agent 对抗:Codex/Claude 轮流实现+审核,交叉验证减少盲区

  5. 退火重试:API 不稳定时脚本自动退避重试,无需人工干预

8 设计灵感

  • karpathy/autoresearch — 核心循环:只保留可测量的改进,其余全部回滚

  • acpx — Agent 控制工具,让 Codex/Claude 在命令行中协作

  • imclaw — 本项目和autoresearch文件github.com/smallnest/i…

从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI -- 肘子的 Swift 周报 #132

作者 东坡肘子
2026年4月21日 08:05

issue132.webp

从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI

从 2019 年问世算起,SwiftUI 已经快七年了。它早已脱去了最初几年的稚气,逐渐成为苹果生态开发者的基础能力之一。不过,SwiftUI 的闭源属性也意味着,它的很多运行机制始终不透明。开发者在使用时固然能感受到它的表达优势,但一旦遇到问题,往往很难进一步追踪原因。这种特性也让 SwiftUI 在 AI 辅助编程时代显得有些“吃亏”——相比那些长期暴露在社区讨论、源码和文档中的技术,大模型能参考的高质量材料终究有限。

也正因此,社区一直希望通过开源项目去复刻 SwiftUI:一方面,是希望让 SwiftUI 这套优秀的设计有机会运行在更多平台上;另一方面,也是希望借助复刻过程,对 SwiftUI 的内部机制获得更多理解。最近几年,这方面最受关注的项目无疑是 OpenSwiftUI。在社区持续推进下,它已经补齐了 SwiftUI 的一部分核心实现,并在苹果生态之外的平台上做出了一些实验性探索。虽然距离它的目标显然还有不短的路要走,但它依然是当下开发者理解 SwiftUI 内部机制的重要入口之一。

其实,除了社区之外,一些公司,甚至规模很大的公司,也在过去几年里做过对 SwiftUI 的深入研究和复刻。上周,字节跳动开源了他们的 SwiftUI 复刻项目 DanceUI

我第一次听说这个项目是在 2022 年。当时最让我感到意外的,不是“有人在复刻 SwiftUI”,而是“为什么是字节跳动在做这件事”。后来陆续和参与这个项目的开发者交流后,我大致理解了他们的动机:一方面,他们希望在将声明式开发引入庞大产品体系时获得更强的控制力;另一方面,也希望借由对 SwiftUI 这类优秀框架的研究,把运行时、依赖图和宿主整合等关键能力握在自己手里。和 OpenSwiftUI 相比,DanceUI 更不像一个社区式复刻项目,而更像一套从工程落地出发、反向拆解 SwiftUI 的样本。

更重要的是,过去几年中,DanceUI 已经在字节内部的一些产品模块中进入了生产环境。这意味着它显然不只是一个实验性的玩具,而是一套在性能和稳定性上都经受过一定检验的开发工具。对于 SwiftUI 开发者来说,它也因此提供了另一个理解 SwiftUI 的入口。

当然,这类项目并不适合被简单神化。它们不是 SwiftUI 本身,也不代表苹果官方实现。尤其像 OpenSwiftUI 这样带有强烈研究和兼容性导向的项目,本身就有明确边界;而像 DanceUI 这样的项目,则带着明显的大厂内部工程背景和落地取向。它们都不应该被当成“SwiftUI 真相”的唯一来源。

但这并不妨碍它们成为很好的学习材料。它们都不是 SwiftUI,却都能帮助我们更接近 SwiftUI。跟着开源项目去 dive SwiftUI,本质上不是在找一个“开源替代品”,而是在借这些项目训练自己理解 SwiftUI 的方式。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

别让协议变成“怪物”:iOS 中的接口隔离实践 (Interface Segregation Principle In IOS: How To Prevent A Protocol From Becoming A Prison)

很多开发者可能都经历过类似的过程:项目早期一个精心设计的小协议,随着团队协作与业务演进,逐渐膨胀为难以维护的“怪物”。Pawel Kozielecki 通过一个逐步失控的 UserService 案例,具体展示了胖协议如何在团队协作中引入测试负担、隐性耦合,以及难以推进的重构成本。作者不仅给出了基于小协议组合与渐进迁移的现实方案,也点出了问题的根源:真正危险的,往往不是一次明显的设计失误,而是一连串“这次先加进去也没关系”的合理决定。

在 AI 辅助编程日益普及的背景下,这一问题反而更容易被放大。大模型倾向于依据文件名、协议名进行语义推断,一个模糊或过于宽泛的命名,往往会自然地吸引更多“不那么相关”的职责被不断叠加进去。清晰、准确且克制的命名,正在从代码风格问题,逐渐演变为影响系统边界的重要因素。


为 Text 实现删除线动画 (Animating Strikethroughs in SwiftUI)

为 SwiftUI Text 的删除线或下划线实现动画效果?不少人第一反应可能是基于 overlay + Shape 的方案。不过,这种方式很难正确适配 Dynamic Type 以及多行文本场景。Ashli Rankin 展示了一条更“系统化”的路径:基于 iOS 17 引入的 TextRenderer,直接访问 Text.Layout 的内部结构(行、glyph 等),并通过一个 progress 值在所有行之间累计绘制,从而实现连续、可动画的删除线效果。同时通过实现 Animatable,让 SwiftUI 在状态变化时自动完成插值过渡。

一个更有意思的细节在于:TextField 并不会走 Text 的渲染流程,因此 TextRenderer 无法直接应用。作者通过叠加一个透明的 Text(负责绘制动画)与真实的 TextField,并结合自定义 Layout 强制两者使用一致的换行宽度,最终解决了多行错位问题。


在 SwiftUI 预览中验证可访问性 (Checking accessibility with SwiftUI Previews)

SwiftUI Previews 通常用于检查界面布局,但同样可以在开发阶段快速验证部分可访问性(Accessibility)表现。Rob Whitake 梳理了几种常用途径:例如通过 Xcode Canvas 直接切换深浅色、方向、Dynamic Type 等进行快速检查,或借助 Preview Traits 定义特定的预览环境。文章还提到了一些仅用于 Preview 的私有环境变量(如增强对比度、减少动画、颜色反转等),通过带下划线的 keyPath 可以强制开启这些状态。不过需要注意,这类 API 必须限制在 #if DEBUG 中使用,以避免私有符号进入最终构建,带来审核风险。


一个 UIKit 项目的 SwiftUI 迁移实录

Yusuke Hosonuma 回顾了自己参与一个 UIKit + RxSwift + Coordinator 项目,并在一年多时间里逐步完成大部分界面 SwiftUI 化的经历。文章聚焦于真实项目中的工程取舍:在小团队、低沟通、几乎无文档的条件下,如何通过持续交付、渐进替换与尽量简单的设计,让项目保持可演进性。作者对不少常见做法都给出了很有现实感的反思,例如谨慎对待 protocol 抽象、EnvironmentObject、过早共通化,以及“顺手清理一切旧架构”的冲动。这并非单纯的技术实现总结,而是一篇充满真实感的团队实践复盘。


如何停止一个运行中的 SwiftUI 动画 Cancelling SwiftUI Animations: What Actually Works (And Why)

在 SwiftUI 中,停止一个已经运行的 repeatForever 动画并不像想象中那么简单。无论是使用 .none,还是通过 Transaction 禁用动画,都只能影响新的动画,而无法中断已经存在于渲染系统中的动画。Codelaby 给出了一个可行方案:通过自定义 CustomAnimation,让 animate 返回 nil(表示立即完成),并通过 shouldMerge 接管当前动画,从而实现终止动画的效果。

SwiftUI 会基于状态变化与动画函数自动进行插值计算。所谓“停止”,本质上是用一个新的状态变化去接管当前动画,而不是中断之前的动画。

工具

Swift Institute: 一个人的 Swift 基础设施重写

偶然看到的一个让我震惊的项目。Coen ten Thije Boonkkamp 在过去 9 个月里提交了约 9800 次 git commit,独自构建了一个分为 primitives、standards、foundations 三层、累计近 300 个包的 Swift 生态。目标只有一个——落地他去年提出的 Modern Swift Library Architecture 思想:依赖只能向下、集成发生在核心类型之外、"test what you own, trust what you import"。

一个人、一个构想,通过 AI 来进行尝试、验证。无论最后是否成功,但这是我想看到的 AI 意义。


swift-ast-lint:用 Swift 写 Swift 代码检查规则

Ryu 开发的 swift-ast-lint 不是另一个 SwiftLint,而是一套基于 SwiftSyntax 的自定义 lint 基础设施。它更适合需要编写 AST 级规则的团队,用来补足正则匹配在结构化检查上的局限。

项目支持脚手架生成、参数化规则、路径过滤以及 --fix 自动修复,比较适合处理架构约束、代码组织、模块边界等 regex 很难可靠覆盖的问题。它不太适合只想开箱即用的用户,但对于已经有明确工程规范、又希望把这些规范工具化的 Swift 团队来说,是一个值得关注的项目。

在 AI 辅助开发越来越普遍之后,真正有价值的可能不只是生成能力本身,还包括如何把团队规范和结构约束工具化。

活动

Swift Craft 2026

Swift Craft 是一个由社区驱动的 iOS / Apple 平台开发者大会,将于 5 月 18–20 日在英国 Folkestone 举行。目前议程已经公布,涵盖 Swift、SwiftUI 以及应用架构等多个方向。

相比大型会议,Swift Craft 更偏向小规模与深度交流,也更强调开发者之间的社区氛围。一个有趣的细节是本次会议的场地:位于海边悬崖上的 Leas Cliff Hall,会场三面落地窗直面英吉利海峡,这种环境本身就足以让会议体验变得与众不同。

主办方为本周报读者提供了折扣码 FBM26(£50 off Indie 票) 。如果你有参与线下开发者活动的计划,可以通过 Swift Craft tickets page 了解详情。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

读完 Claude Code 源码才发现:Skills、MCP、Rules 的区别,远没有你想的那么大

作者 百度Geek说
2026年4月17日 10:12

导读 introduction

通过对Claude Code源码的分析,揭示了Rules、MCP、Skills三个概念的底层实现机制。Rules是项目级行为规范,通过messages被动注入;MCP是标准化工具协议,在system和tools中注册并调用外部服务;Skills是可复用提示词,通过tool_use触发后注入指令文本。三者的核心区别在于信息在API请求中的位置不同,而非功能本质...

01 背景

1.1 概念爆炸:学不完的新名词

如果你在用 Claude Code、Cursor 或其他 Coding Agent,你一定经历过这样的感受——

刚弄明白怎么写 Rules 让 Agent 听话,MCP 就火了,一堆人说"MCP 才是未来";MCP 的 Server 还没配明白,Skills 又冒出来了,号称"标准化工作流"。每隔几周就有新概念冒出来,配上各种似是而非的定义,让人焦虑:这些东西之间到底是什么关系?我是不是又落伍了?

1.2 越看越糊涂的"官方定义"

网络上流行的定义往往加剧了混乱:

  • "Rules 是项目级的行为规范"
  • "MCP 是标准化的工具协议"
  • "Skills 是可复用的标准化工作流"

看完这些,你可能和我一样,产生了更多疑问:

  • Rules vs Skills:都说 Skills 的优势是"按需引入",但 .claude/rules/ 里的条件规则不也能按路径按需生效吗?它们的区别到底在哪?

  • MCP vs 内置 Tools:MCP 工具和 Claude Code 自带的 Read、Edit、Bash 这些内置工具,对模型来说有什么不同?为什么需要一套新协议?

  • Skills 的"标准化流程":所谓流程化,是真的像代码一样有 if-else 和循环控制的?还是只是一段写得好的提示词?

1.3 从源码找答案

这些问题靠读文档和博客是答不清楚的,因为它们本质上是实现层面的问题

恰好 Claude Code 的源码在 GitHub 上泄漏,本文基于 v2.1.88 泄漏源码,从 LLM API 调用层面,拆解 Rules、MCP、Skills 的底层实现。看完源码你会发现,这三个概念远没有网上说的那么玄乎,它们的区别,本质上就是信息在 API 请求中被塞到了不同的位置。

02 前置需要了解的

为了避免阅读本文的读者对一些 Agent 中的流程不够了解,先介绍一下相对重要的知识点。

2.1 Agent 与 LLM API 的交互协议

图片

每次 Agent 调用 LLM,本质上就是发一个 HTTP 请求,请求体由三个核心参数组成:

anthropic.messages.create({
    system: TextBlockParam[], // 静态角色定义和行为规范
    
    tools: BetaToolUnion[], // 工具定义(name + description + input_schema)
    
    messages: MessageParam[], // 动态对话内容
})

2.1.1 system — "你是谁,你该怎么做"

定义模型的角色和行为规范。在 Claude Code 中,system 包含:

  • 核心系统提示(行为规范、编码风格、安全规则等)

  • Git 状态信息(通过 appendSystemContext 追加)

  • MCP Server 级 instructions(若 Server 提供了使用说明,追加在动态区域中)

system 提示分为静态部分(可跨用户缓存)和动态部分(因会话而异,不参与缓存共享)。MCP instructions 属于动态部分。

system 的静态部分高度稳定,可利用 Anthropic 的 org 级 Prompt Cache。 同一份静态内容只需计算一次 KV 矩阵,所有用户共享缓存,后续调用仅需 0.1x 费用。CLAUDE.md 等因项目而异的内容不放在 system 里,就是为了不破坏这份共享缓存。

2.1.2 tools — "你能做什么"

tools 数组定义模型可以调用的工具。每个工具包含 namedescription(来自工具的 prompt() 方法输出)、input_schema。模型根据工具描述决定何时调用哪个工具。

内置工具和 MCP 工具在这里的格式完全一致,模型无法区分它们——区别只在 Agent 侧的执行路由。

2.1.3 messages — "对话发生了什么"

messages 是一个 user/assistant 交替的消息数组,但在 Claude Code 中,它远不只是"对话历史"。实际混合了三种内容:

  • 系统上下文注入prependUserContext):CLAUDE.md 内容、当前日期等

  • 系统提示上下文appendSystemContext):git 状态等(注入到 system 参数)

  • 动态附件(Attachments):Skill 列表、计划模式指令、子目录 CLAUDE.md

  • 真实对话历史:用户输入、模型回复、工具调用结果

前两类都以 isHidden: true + isMeta: true 注入,用 <system> 标签包裹。isHidden 是客户端侧的 UI 标记,消息仍完整发送给 API,但不会在终端界面中展示给用户。<system> 不是 API 特殊字段,而是 Claude Code 与模型之间的约定格式,系统提示词中会告知模型"被此标签包裹的内容权重等同于系统指令",让模型能区分系统注入的指令和用户真正说的话。

为什么系统上下文不放在 system 里?因为 CLAUDE.md 等内容因项目而异,混入 system 会破坏 org 级共享缓存。放在 messages 中,既不影响 system 缓存,又能在会话内轮次间复用。

2.2 tool_use:一切扩展机制的底层基础

Claude 的工具调用本质上是一个结构化的多轮对话协议

用户消息
↓
模型推理 → 输出 tool_use 块
{ "type": "tool_use", "id": "toolu_xxx", "name": "工具名", "input": { ...参数... } }
↓
调用方(Agent)执行工具
↓
将结果作为 tool_result 追加到对话
{ "type": "tool_result", "tool_use_id": "toolu_xxx", "content": "执行结果" }
↓
继续下一轮模型推理

模型本身不"执行"任何工具,它只是输出一段结构化 JSON,真正的执行发生在调用方(即 Claude Code 客户端)。理解了这一点,就能理解为什么 Rules、MCP、Skills 虽然表现形式完全不同,但底层都构建在同一套 tool_use 协议之上。

图片

03 实现细节

3.1 Rules(CLAUDE.md)的实现

3.1.1 Rules 是什么

Rules 就是 CLAUDE.md 文件(以及 .claude/rules/*.md 规则文件)。它们是用自然语言写的指令文本,告诉模型"在这个项目中你应该遵循什么规范"。

3.1.2 文件发现机制

Claude Code 从多个位置按优先级加载 Rules(源码中对应 getMemoryFiles 函数)。实际加载逻辑是从项目根到 CWD 逐层处理,每层内部按 CLAUDE.md.claude/CLAUDE.md.claude/rules/*.mdCLAUDE.local.md 的顺序收集,后加载的覆盖先加载的。主要来源包括:

图片

单个 CLAUDE.md 文件建议不超过 40,000 字符,超出会触发诊断警告⚠️。

3.1.3 内容处理流程

每个文件经过 processMemoryFile 处理:

读取文件
↓
解析 frontmatter(提取 paths 等条件匹配字段)
↓
移除 HTML 注释
↓
处理 @include 引用(最大递归深度 5 层)
↓
条件规则匹配(.claude/rules/*.md 中 paths 字段匹配当前文件路径)
↓
格式化输出

条件规则是一个值得注意的特性。在 .claude/rules/ 下的规则文件可以通过 frontmatter 中的 paths 字段指定生效范围:

---
paths:
- "src/components/**/*.tsx"
- "src/hooks/**/*.ts"
---
 React 组件中始终使用函数式组件和 hooks。

这意味着这条规则只在模型处理匹配路径的文件时才会被注入。

3.1.4 注入方式:进入 messages,而非 system

格式化后的 Rules 内容通过 prependUserContext() 注入到 messages 的最前面,包裹在 <system-reminder> 标签中,以 role: "user" + isMeta: true 的形式存在——isMeta 是客户端 UI 标记,消息本身仍完整发送给 API,但不会在终端中展示给用户。

注入时还会带上一个强制指令头:

"Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written."

核心洞察:Rules 不走 ****tool_use**** 协议。 它既不是工具,也不需要模型主动调用。它是被动注入到每次 API 调用的上下文中,模型在推理时自然会"看到"并遵循这些规则。

具体的 prependUserContext() 源码还原见 [[Claude Code 架构解析:从 Skill 调用到 Prompt Cache]]

3.1.5 子目录 Rules 的动态加载

当模型在对话过程中访问了某个子目录的文件,Claude Code 会检查该子目录是否有 CLAUDE.md。如果有,会通过 nested_memory attachment 动态注入:

// nested_memory attachment 处理
case "nested_memory":
return [createMessage({
    content: `Contents of ${attachment.content.path}:\n\n${attachment.content.content}`,
    
    isMeta: true
})];

这实现了 Rules 的按需加载——只有当模型实际接触到某个子目录时,该目录的规则才会被加载进来。

3.2 MCP Tools 的实现

3.2.1 MCP 是什么

MCP(Model Context Protocol)是一个标准化协议,让 Claude Code 能调用外部服务提供的工具。它是 tool_use 最直接的应用——模型触发后,客户端向外部 MCP Server 进程发起 RPC 调用,拿到真实结果。

3.2.2 配置与连接

MCP 服务器定义在 ~/.claude.json(user scope)或项目根目录的 .mcp.json(project scope)中,常见传输方式:

图片

连接建立后,Claude Code 通过 MCP SDK 与 Server 完成 initialize 握手。这一步不仅获取工具列表,还会拿到 Server 返回的 instructions 字段——一个可选的 Server 级使用说明,后面会看到它的去向。

3.2.3 MCP 在 API 请求中占据两个位置

MCP 不只是注册在 ****tools[]**** 里,它还在 ****system**** 中有一席之地。

位置一:tools[] — 工具定义

每个 MCP 工具通过 toolToAPISchema() 转换为 tools[] 格式,命名遵循 mcp__<serverName>__<toolName> 模式:

// toolToAPISchema 核心逻辑
async function toolToAPISchema(tool, options) {
    return {
    
        name: tool.name, // 如 "mcp__github__create_issue"
        
        description: await tool.prompt(), // 工具描述 → tools[].description
        
        input_schema: tool.inputJSONSchema // 参数 schema
    
    };
    
}

这部分和内置工具的注册方式完全一致,模型通过工具描述决定何时调用。

位置二:system — Server 级 instructions

在系统提示词的构建过程中,getMcpInstructions() 会将所有已连接 Server 的 instructions 拼接进 system 的动态区域(位于缓存边界标记之后):

// getMcpInstructions(源码路径:src/constants/prompts.ts)
function getMcpInstructions(mcpClients) {
const clientsWithInstructions = mcpClients
.filter(c => c.type === "connected")
.filter(c => c.instructions); // 只取有 instructions 的 Server
if (clientsWithInstructions.length === 0) return null;
return `
# MCP Server Instructions
  
The following MCP servers have provided instructions for how to use their tools and resources:
  
${clientsWithInstructions.map(c => `## ${c.name}\n${c.instructions}`).join("\n\n")}
`;
}

当 feature gate isMcpInstructionsDeltaEnabled() 开启时,MCP instructions 会改走 attachment 注入而非 system,以避免 Server 连接/断开破坏 prompt 缓存。

MCP Server 可以通过 initialize 响应的 instructions 字段,向模型传达整个 Server 级别的使用指南,比如"优先使用 search 工具而非 list 工具"、"所有日期参数必须用 ISO 格式"等。这些指导信息是全局性的,不是针对单个工具的。

tools[].description 描述的是"这个工具做什么、参数是什么",system 中的 instructions 描述的是"如何正确地使用这个 Server 的工具集"。一个是单工具说明书,一个是整体使用手册。

3.2.4 执行流程

MCP 工具的调用是真正的函数调用

模型输出 tool_use: { name: "mcp__github__create_issue", input: {...} }
↓
Claude Code 识别 mcp__ 前缀,路由到对应 MCP Client
↓
MCP Client 发送 JSON-RPC 请求到 MCP Server 进程
↓
MCP Server 执行实际操作(如调用 GitHub API)
↓
返回真实结果
↓
tool_result.content = MCP Server 的真实输出
↓
模型读取结果,继续推理

MCP 是名副其实的"远程过程调用"。 工具做真实的事情,结果回传给模型。tool_result 里装的是外部世界的真实数据

3.2.5 MCP 祛魅:很多场景下一条 Bash 就够了

理解了源码实现后,一个自然的问题浮出水面:模型已经有 Bash 工具了,为什么还需要 MCP?

对模型来说,调 mcp__github__list_issues 和执行 gh issue list 拿到的结果没有本质区别——都是 tool_result 里的一段文本。但 MCP 多了一个 Server 进程、一层 JSON-RPC 通信、一套配置和维护成本。实际使用中,查 GitHub 用 gh,读数据库用 psql,调 API 用 curl,大量 MCP Server 做的事一条命令就能替代。

那 MCP 真正不可替代的场景是什么?

  1. 持久化连接和状态管理:Bash 每次是新进程没有状态。数据库连接池、WebSocket 长连接、跨调用共享认证 session,MCP Server 作为常驻进程可以做到

  2. 复杂操作的原子封装:把 5 步 Bash 命令封装成一次 MCP 调用,减少模型拼长命令出错的概率

  3. 权限隔离和安全约束:Bash "什么都能干",MCP Server 可以限制模型只执行预定义操作

MCP 的价值不在于"能调用外部系统"(Bash 也能),而在于"以更安全、更可靠的方式调用外部系统"。

3.3 Skills 的实现

3.3.1 Skills 是什么

Skills 是可复用的 Markdown 提示词文件(SKILL.md),定义了一套结构化的工作指令。它同样通过 tool_use 触发,但执行逻辑与 MCP 截然不同。

3.3.2 文件发现

Skills 从以下位置扫描发现:

图片

3.3.3 Skill 列表注入

模型怎么知道有哪些 Skill 可用?通过 skill_listing attachment 注入到 messages 中:

// skill_listing attachment 处理
case "skill_listing": {
    return [createMessage({
    
        content: `The following skills are available for use with the Skill tool:\n\n${attachment.content}`,
        
        isMeta: true
    
    })];
}

Skill 列表有严格的 token 预算:仅占上下文窗口的 1%(默认 8000 字符),每个 Skill 描述最多 250 字符。当 Skill 数量过多时,描述会被截断甚至移除。这是为了避免 Skill 列表挤占对话空间。

同时,Skill 工具的 description 中包含一条强制触发指令(BLOCKING REQUIREMENT):

"When a skill matches the user's request, this is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task"

这条指令确保模型看到匹配的 Skill 时,必须先调用工具,不能直接回答。

3.3.4 执行流程:提示词注入,不是函数调用

当模型调用 Skill 工具时,默认走 Inline 模式

模型输出 tool_use: { name: "Skill", input: { skill: "commit", args: "" } }
↓
Claude Code 读取本地 SKILL.md 提示词文本
↓
将提示词内容包装为 isMeta: true 的 user 消息,注入到对话历史中
↓
tool_result 仅返回一个标签:"Launching skill: commit"
↓
下一轮 API 调用时,对话历史中已包含完整的 Skill 指令
↓
模型读到指令后,按步骤调用工具(Read、Edit、Bash 等)执行任务

3.3.5 Inline 模式 vs Fork 模式

Skills 有两种执行模式,Inline 是默认模式,Fork 需要 Skill 配置文件中显式设置 context: 'fork' 才会触发:

图片

Fork 的隔离性意味着 Skill 内部的文件缓存、权限拒绝记录、abort 控制都是独立的,不会污染主对话上下文。

核心洞察:Skills 是"提示词注入"机制,不是函数调用。tool_use 只是触发器,真正的"能力"来自被注入的 Markdown 指令文本。模型读到指令后,按指令一步步执行,利用已有的工具(Read、Edit、Bash 等)完成任务。

04 总结

4.1 三者的核心对比

图片

4.2 一张图理解全貌

图片

4.3 回答开头的三个问题

Q1:Rules 和 Skills 都支持按需引入,区别在哪?

先说结论:区别没有想象中大。 从源码看,Skills 执行后注入的就是一段 Markdown 提示词,和你手动把一段 Rules 文本贴进对话框,对模型来说没有本质区别——都是 messages 里的一段 role: "user" 文本。

真正的区别只有两点工程实现上的差异:

  1. 触发方式:Rules 每次 API 调用自动注入,Skills 需要模型判断后主动调用 tool_use(或用户手动 /skill-name 触发)

  2. 执行隔离:Skills 可配置在 Fork 上下文中运行,拥有独立的缓存、权限跟踪和 abort 控制;Rules 没有这层隔离

但现实中,第一点反而成了 Skills 的痛点。模型判断"是否需要调用 Skill"依赖的是 skill_listing 中最多 250 字符的描述加上 whenToUse 字段——这点信息经常不够模型做出正确判断。这就是为什么很多人发现 LLM 不会自动触发 Skill,最终还是靠手动 /commit/review-pr 来调用。

想想这意味着什么:如果你每次都是手动触发,那 Skills 的完整调用链路是这样的:

你输入 /commit
→ Claude Code 查找对应 SKILL.md
→ 包装为 tool_use 调用
→ 读取 Markdown 文本
→ 注入到 messages 中
→ 模型读到这段文本,按指令执行

而你手动用 @commit-rules.md 引用一个同等内容的 Rules 文件,效果是:

你输入 @commit-rules.md + "帮我提交代码"
→ Claude Code 读取文件内容
→ 作为 FileAttachment 注入到 messages 中
→ 模型读到这段文本,按规范执行

两者最终模型看到的都是一段自然语言指令,没有本质区别。 Skills 多绕的那几步(tool_use → 读文件 → 注入),本质上只是提供了额外的工程便利。如果你每次都是手动 /commit,那和直接 @commit-rules.md 效果几乎一样。

那 Skills 真正有价值的场景是什么?关键在于手动引用 Rules 替代不了的三个点:

1. 模型自主触发——用户只需表达意图

当 Skill 的 description/whenToUse 写得足够精准,模型能自动识别场景并触发,用户不需要知道这个 Skill 的存在。差距在单步场景不明显,但在多步骤组合任务时就凸显了:

用户:"帮我完成这个 feature,包括写代码、写测试、提交"
  
手动引用方式:
@coding-rules.md @test-rules.md @commit-rules.md
→ 用户需要知道有哪些规则、叫什么名字、在哪里
  
Skill 自动触发:
→ 模型识别任务,依次自动调用 coding / test / commit skill
→ 用户只说了目标,工具选择完全交给模型

Skills 还支持嵌套调用(Skill 内部再触发其他 Skill),可以用一个"主 Skill"编排多个子 Skill,形成完整的多步工作流入口。

注意:自动触发能力的上限取决于 Skill 描述的质量,而不是 Skill 数量。描述模糊或触发时机不清晰的 Skill,模型大概率不会自动识别,最终还是要靠手动 /skill-name 触发——此时就和手动引用 Rules 没有区别了。

2. 可发现、可分发——团队协作的标准化入口

Skill 有名字、注册在系统里,可以通过 /skills 浏览,可以打包进插件发布给团队。Rules 文件路径是私人知识,Skill 是"被组织管理的知识"。当你需要把一套工作流标准化并推广给不了解内部实现的团队成员时,Skill 是更合适的载体——用户只需记住 /commit,不需要知道背后引用了哪些规则文件。

3. Fork 模式的独立执行生命周期——这是手动引用 Rules 做不到的

配置 context: 'fork' 后,Skill 在独立 Agent 上下文中运行:执行过程中所有的 tool_use/tool_result 不会写入主对话,主对话保持干净;有独立的 abort 控制和权限跟踪,不会影响主流程。长流程多步任务特别适合 Fork 模式。

Q2:MCP 和 LLM 内置 Tools 的区别在哪?

对模型来说没有区别。tools[] 里格式一样,调用方式一样。区别纯粹在 Agent 侧的执行路由:内置 Tools 本地执行,MCP Tools 转发到外部 Server。

如上文 2.5 所述,大多数简单场景 Bash 就能替代 MCP。MCP 真正的价值在持久化连接、原子封装和权限隔离三个点上。另外 MCP 的 Server 级 instructions 注入到 system 中理论上能提供工具集使用指南,但现实中大多数 Server 作者根本没写这个可选字段。

Q3:Skills 的标准化流程是"代码层面的流程化"吗?

不是。 源码里没有任何代码逻辑来控制 Skill 的执行步骤。所谓"标准化工作流",就是一段写得比较结构化的 Markdown——"Step 1 做什么,Step 2 做什么"。模型读到后自行理解、自行执行,完全靠模型的指令遵循能力。

这意味着:

  • Skill 的质量 = 提示词的质量

  • Skill 的"流程保障"= 模型的指令遵循率

  • 同一个 Skill,换一个弱一点的模型,流程可能就乱了

从这个角度看,写一个好的 Skill 和写一段好的 Rules,需要的能力是一样的——都是提示词工程

4.4 实际使用建议

基于源码分析和实际使用经验,给出一些落地建议:

什么时候用 Rules:

  • 项目级的编码规范、技术栈约定、代码风格要求

  • 文本短(几百字以内),每次注入不心疼 token

  • 需要"始终生效"的指令,不依赖模型判断是否需要

什么时候用 Skills:

  • 指令文本较长(几百行级别),不适合每次注入

  • 有明确的触发时机(用户主动 /commit/review-pr

  • 需要执行隔离(Fork 模式能让任务在独立上下文中运行,不污染主对话)

什么时候用 MCP:

  • 需要持久化连接/状态管理的场景(数据库连接池、认证 session)

  • 复杂多步操作需要原子封装,减少模型拼命令出错的概率

  • 需要权限隔离,不想给模型一个万能的 Bash

  • 如果只是简单的 CLI 操作(ghcurlpsql),直接让模型用 Bash,别折腾 MCP

一个现实提醒: 不要迷信 Skills 的自动触发。源码中 Skill 列表的 token 预算只有上下文的 1%,每个描述最多 250 字符。如果你的 Skill 描述写得不够精准,或者用户意图不够明确,模型大概率不会自动触发。把核心 Skill 的快捷命令告诉团队成员,让他们手动调用,比指望模型自动识别靠谱得多。 MCP 同理——在引入之前先想想,Bash 能不能直接搞定。

参考源码:

claude-code-source-code

v2.1.88(泄漏源码)

github.com/anthropics/…

立正请站好:一个组件复用 Skill 的工程化实践|得物技术

作者 得物技术
2026年4月14日 09:53

一、背景:为什么要做这个 Skill

做这个 Skill 的初衷很直接,也很现实:功能开发时容易"顺手新建一个",而不是先复用已有组件,造成组件库越来越臃肿。这件事对团队的伤害其实是复利型的:

  • 重复组件越来越多;
  • 维护成本越来越高;
  • UI/交互一致性越来越差;
  • AI 生成代码时也更容易继续复制混乱。

所以做这个 Skill 的目标不是"帮 AI 搜索一下",而是:把"复用优先"的思考过程流程化,让 AI 在写代码前先走一遍"查索引 → 判断是否复用 → 命不中再新建"的路径。

二、想解决的不是搜索问题,而是“思考顺序”问题

一开始很容易把问题理解成:"做个组件搜索工具给 AI 用就好了"。但实际落地后发现,真正的问题不是工具有没有,而是:

  • AI 会不会主动用;
  • AI 什么时候用;
  • AI 用完之后是否还能回到项目上下文;
  • AI 能不能稳定走同一条流程。

这和 Vercel 在他们的 agent 评测里观察到的现象很像:skills 本身不是没用,而是 agent 往往不会稳定触发;而把基础知识放进 AGENTS.md 这种"被动上下文"后,稳定性反而更高。Vercel 的实验里,默认 skill 触发并没有提升通过率,加入显式指令后才明显改善,而 AGENTS.md 文档索引方案表现更稳定。这给了我一个很关键的设计方向:先解决 AI 的"决策点"问题,再解决 AI 的"能力"问题。

三、核心设计思路:AGENTS.md + Hook + Skill(三层结构)

最终采用的是三层结构:

AGENTS.md:放基础上下文(常驻)

把"组件复用优先"的规则、组件索引入口、扫描后需要做的事情,放进 AGENTS.md(或同类常驻上下文机制)里。目的不是塞满文档,而是让 AI 每轮都知道:

  • 这个仓库有组件复用机制;
  • 默认应该先查可复用组件;
  • 查不到再考虑新建;
  • 扫描后还有描述补全流程需要继续执行。

这层解决的是:AI 根本不知道你有这套机制。不写进去,AI 主动使用 skill 的概率确实会很低(这点我踩过坑)。

Hook:做路由增强(提高触发概率)

如果运行环境支持 hooks(例如 Claude Code 的 UserPromptSubmit 支持在用户 prompt 处理前注入额外上下文),就可以做一层"意图路由增强":在用户提到"组件复用 / 是否有现成组件 / 封装组件 / 查组件"等语义时,给 AI 注入提示,让它优先走组件复用流程。Claude 的文档明确写了 UserPromptSubmit 会在处理前触发,并且可通过 additionalContext 注入上下文。这层解决的是:AI 知道有 skill,但不一定想起来用。

Skill:提供流程和工具(真正执行)

Skill 不是只写说明文档,而是要提供:

  • 明确的调用入口;
  • 稳定的输出格式;
  • 可执行脚本;
  • 失败时的兜底逻辑。

OpenAI 的 Codex Skills 文档里提到 skills 是"渐进披露"机制:运行时先看到 skill 的元信息(尤其是 description),只有决定使用时才加载完整 SKILL.md;而且隐式触发高度依赖 description。这也是为什么 skill 的触发边界和描述要写得非常清楚。这层解决的是:AI 想用了,但执行过程不稳定。

四、这套 Skill 在源码里是怎么落地的(我的实现)

下面是我这次组件复用 Skill 的几个关键实现点:

先把"入口"收敛成一个:find-component.js

我在 SKILL.md 里明确规定:Agent 必须调用统一入口find-component.js。这样做的原因很简单:

  • 避免 AI 在多个脚本之间犹豫(scan-components、match-component、resolve-scope……);
  • 避免 AI 漏掉前置步骤(比如索引不存在时先扫描);
  • 避免 AI 调用路径不一致导致结果不稳定。

统一入口做了几件事(都在 find-component.js 里):接收查询词(query)、仓库根路径(repoRoot)、当前聚焦路径(startDir)。

  • 如果 components.csv 缺失,内部自动触发run-scan.js;
  • 调用 resolve-scope 计算当前应用和允许搜索范围;
  • 调用 match-component 做匹配排序;
  • 命中时记录使用(用于后续加权);
  • 按固定 JSON 协议返回结果(成功/失败/无匹配/是否触发扫描等)。

这一步本质上是把分散逻辑聚合成"一个业务动作":"查一下有没有可复用组件",而不是"先算 scope,再查 CSV,再排序,再补扫,再记 usage"。这对 AI 很关键。

不是"全仓库乱搜",而是"当前应用 + 根级共享"优先

在 monorepo 场景里,组件复用很容易踩两个坑:

  • 只搜当前 app,漏掉根级共享组件;
  • 全仓乱搜,结果太多太噪音。

所以我在 resolve-scope.js 里做了一个比较工程化的范围解析策略:

  • 读取 pnpm-workspace.yaml 解析 workspace 包;
  • 根据当前聚焦文件/目录反推 currentAppRoot;
  • 再结合 root_scope_patterns(例如 apps/_share/、packages/ 等)构建允许范围;
  • 最终形成一个搜索集合:当前应用 + 根作用域共享包。

如果没有聚焦子项目(比如 startDir 就是 repo root),则切换为全量 scope。这个设计很像人类工程师的查找策略:先看"我这个业务应用里有没有",再看"全局共享有没有",而不是直接在整个 monorepo 海里捞针。

匹配不是纯关键字:我做了"多因素加权"

组件匹配如果只做字符串包含,很快就会变成垃圾召回器。我在 match-component.js + fuzzy-match.js 里做了一个组合评分,核心包括:

  • 名称精确/包含匹配;
  • 模糊匹配(编辑距离);
  • Token 重叠;
  • 首字母缩写匹配(例如 dlp 匹配 DateLinkPicker);
  • 当前应用加权(当前 app 的组件优先);
  • 使用频率加权(常用组件更靠前);
  • 来源质量加权(README 推断质量高于纯 inferred);
  • 存在性校验(文件不存在则降权/过滤);
  • 记录类型权重(组件优先于依赖)。

这一步的目标不是追求"算法先进",而是让排序更符合团队真实使用习惯:"更可能被复用的组件排在前面"。此外我还加了一个低分阈值(NO_MATCH_SCORE_THRESHOLD):

  • 如果最高分太低,就认为是噪音命中;
  • 可以触发一次扫描后再查;
  • 还是低分则按"无匹配"返回,不把噪音结果塞给 AI。

这个点很重要,因为 AI 一旦拿到一些低质量候选,很容易"将错就错"。

把"索引构建"做成可复用流水线,而不是一次性脚本

很多类似方案停在“扫一遍生成 CSV”,然后就过时了。我这次把扫描做成了 run-scan.js -> index-manager -> enrich 的流水线,核心考虑是持续维护:

run-scan.js 负责编排流程

  • resolve-scope;
  • updateIndex;
  • 自动触发 autoEnrich(可配置)。

index-manager.js 负责索引更新策略

  • 保留历史记录并合并;
  • 根据 source_hash 跳过未变化组件;
  • 记录 last-scan-changed-ids.json;
  • 支持并行扫描(包数量较多时启用);
  • 对缺失文件支持标记 exists=0(在查找阶段也会回写)。

扫描后进入 Agent 富化(enrich)流程

  • 读取 agent-enrich-prompts.json;
  • 找出 summary 占位符项;
  • 按 id 回到 components.csv;
  • 读取源码/README;
  • 生成 summary + keywords;
  • 再通过 update-component-summary.js 写回。

更关键的是在配置里启用了:

  • agent_mode_no_fallback = true。

也就是说,在 Agent 模式下不走规则引擎降级,而是要求 Agent 必须完成这一步。这其实就是"流程化思考"的精髓:不是建议,而是纳入主流程。

让 Skill 不只是"搜索器",还是"反馈回路"

一个很容易被忽视的点是:查找命中后,我还记录了使用行为(usage-tracker)。这意味着系统不是静态的,它会逐步学习团队偏好:

  • 哪些组件经常被复用;
  • 哪些组件在某个 app 里更常出现;
  • 哪些结果应该在排序中更靠前。

这是一种很轻量但非常实用的反馈机制——不需要搞复杂训练,也能提升 AI 下一次推荐质量。

五、这次实现里,总结出"让 AI 流程化"的 3 条原则

这也是我最想分享的部分:

原则 1:把基础上下文放进 AGENTS.md(或用 Hook 注入)

如果不这样做,AI 主动使用 skill 的概率很低。原因不是 AI 笨,而是 agent 的执行是有"决策成本"的:

  • 它要先意识到有 skill;
  • 再判断该不该用;
  • 再决定什么时候用。

而把基础上下文放进 AGENTS.md 或通过 hook 提前注入,本质上是在减少决策点。Vercel 的评测结果说明了这种"被动上下文"在某些场景下会更稳定。

原则 2:Skill 需要直接提供工具函数给 AI 调

只写一堆说明文档不够。AI 在工程任务里最需要的是:

  • 一个可以直接执行的入口;
  • 明确的参数;
  • 稳定的返回结构。

所以我把 find-component.js 做成统一入口,并定义了固定 JSON 输出(ok / matches / noMatch / scanTriggered / hint / error 等),这会明显提升 AI 的执行稳定性。

原则 3:显式告诉 AI 调哪些函数,并把分散逻辑聚合到一个入口

这是最容易被忽略、也是最影响稳定性的一点。如果给 AI 暴露一堆脚本:

  • resolve-scope.js;
  • match-component.js;
  • run-scan.js;
  • scan-components.js;
  • index-manager.js。

它理论上能拼起来,但实践里很容易漏步骤、顺序错、参数错。所以我在 Skill 里显式规定:

  • 查找时用 find-component.js;
  • 构建时用 run-scan.js;
  • 更新描述时用 update-component-summary.js。

把复杂系统收敛成几个明确入口,AI 才容易稳定执行。

六、这次实践里一个很重要的认知转变

我原来以为"写 skill"是在给 AI 增加能力。现在更像是在做:给 AI 增加"默认工作方式"。换句话说,skill 不只是能力包(capability bundle),也是流程控制器(workflow controller)。

  • AGENTS.md 负责"告诉 AI 世界观";
  • Hook 负责"提醒 AI 现在该用哪套流程";
  • Skill 负责"把动作做完,并且做得稳定";
  • 日志/CSV/usage 负责"让系统可观测、可迭代"。

这套思路不只适用于组件复用,后面也可以迁移到:

  • 任务优化闭环;
  • 日志分析标准化;
  • 策略诊断流程;
  • 代码规范治理。

七、这套方案当前的价值

  • AI 开发前先查可复用组件,而不是直接新建;
  • monorepo 下按"当前应用 + 共享组件"范围检索;
  • 索引缺失自动扫描;
  • 组件描述富化进入主流程;
  • 匹配质量有加权与反馈回路;
  • 整体流程有明确入口和输出协议。

八、结语:让 AI 少一点"即兴发挥",多一点"工程纪律"

这次组件复用 Skill 的开发过程,对我最大的启发不是"AI 能帮我写多少代码",而是:AI 其实非常适合被放进一套清晰流程里工作。只要把下面三件事做好:

  • 基础上下文(AGENTS.md / hooks);
  • 可执行入口(工具函数);
  • 明确流程边界(统一入口 + 输出协议)。

AI 就不会只是"一个会说话的代码补全器",而会更像一个遵守团队规范的工程协作者。而这,才是我做这个 Skill 真正想要的结果。

引用文档: vercel.com/blog/agents…

往期回顾

1.财务数仓 Claude AI Coding 应用实战|得物技术

2.日志诊断 Skill:用 AI + MCP 一键解决BUG|得物技术

3.Redis 自动化运维最佳实践|得物技术

4.Claude在得物App数仓的深度集成与效能演进

5.Claude Code + OpenSpec 正在加速 AICoding 落地:从模型博弈到工程化的范式转移|得物技术

文 /魏无涯

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131

作者 东坡肘子
2026年4月14日 07:56

issue131.webp

被 Vibe 摧毁的版权壁垒,与开发者的新护城河

Anthropic 不久前宣布,由于其最新模型 Mythos 在网络安全与代码漏洞挖掘方面的能力“过于强大”,已达到令人不安的程度,因此采取了极为罕见的克制措施:仅向 Project Glasswing 内的少数关键基础设施企业开放,不面向公众发布,普通开发者也无法通过 API 调用(当然,也有分析者指出,这一安排同样有助于防止模型蒸馏,并锁定企业级客户)。但即便这头“猛兽”被暂时按住,当前主流 AI 模型的代码能力,已经足以让复制一款产品变得轻而易举。

上周,Reddit 上一位开发者宣称,自己花了一年时间“逆向 SwiftUI API”,打造了一个全新的 Swift Web 框架。帖子行文流畅、术语考究,一度吸引了不少关注。但 Paul Hudson 很快现身评论区打假:所谓“独立研发”,实际上只是将他的 MIT 开源项目 Ignite 做了简单的字符串替换,甚至连原作者带有个人风格的代码注释都原封不动保留,随后将整个仓库压缩为一次提交以抹除历史,并违规改为具有传染性的 GPL 协议。社区中不少开发者都怀疑,这套“逆向 SwiftUI”的叙事本身也是由 AI 生成。更耐人寻味的是,该作者本就是 Ignite 的主要贡献者之一——当 Vibe Coding 将“重新打包一个项目”的成本降至极低时,“我参与过”本身也可能成为一种模糊责任边界的话术。

几乎在同一时间,macOS 上精致的 AI 工作状态监控应用 Vibe Island 在发布后不久便遭遇了像素级仿制。尽管仿制者打着“开源替代品”的旗号公开了代码,这依然对原作者的商业销售与创作热情造成了明显冲击。然而,即便作者希望采取法律手段,也将面临一个新的时代难题:在确权与维权过程中,他可能需要证明其作品具备足够的人类独创性,并说明 AI 生成内容的参与程度,否则将面临更高的不确定性。

事实上,代码的法律壁垒正从“确权端”开始松动。上个月,中国版权保护中心正式启用新版《计算机软件著作权登记申请表》及相关审查新规,明确要求经办人实名承诺“未使用 AI 开发编写代码、撰写文档或生成登记材料”,并在审查中重点评估人类智力投入是否达到著作权法所要求的“独创性”门槛;缺乏实质人类参与的内容,将难以获得确权。违规者还可能被纳入失信名单,并与个人征信挂钩。

这一趋势也与欧美近期的判例方向趋于一致:如果一段代码主要由 AI 根据提示词快速“改写或重组”生成,其获得著作权保护的可能性将显著降低。

我们必须承认一个残酷的事实:“我有一个绝妙的 Idea,并把它 Vibe 成代码”,已经不足以构成商业壁垒。这种名为 Vibe Coding 的新范式,不仅改变了开发流程、显著提升了效率,也从三个方向同时动摇了软件版权体系的基础逻辑:确权门槛提高、侵权举证变难、功能复刻被默许。

令人遗憾的是,即便这些克隆项目饱受争议,它们依然在 GitHub 上收获了不菲的 Star。这说明,在获取成本极低的前提下,仅靠道德呼吁,已经很难阻止人们对“免费平替”的追逐。

或许,正如我们在 第 120 期周报中讨论 Skip 的开源举措 时所提到的那样——在代码实现成本趋近于零、应用随时可能被 AI 一键克隆的当下,闭门造车“卖工具”将变得愈发困难。与用户建立真实连接,将“出品方的信誉与社区的信任”转化为不可复制的品牌资产,或许才是未来开发者真正的核心竞争力与护城河。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

Swift Blog Carnival: Tiny Languages

Swift 社区正在发起第一届 Blog Carnival,四月的主题是 Tiny LanguagesChristian Tietze 邀请开发者围绕这一主题撰写博客:自定义 DSL、Result Builder、脚本解析器、路由规则……任何与“微型语言”相关的思考都可以作为切入点,截止日期为 5 月 1 日。

目前已有三篇投稿:

  • Matt Massicotte 回顾了他 从 Rake 到 Make,再到各类 Swift 任务运行器的探索历程,坦言至今仍未找到理想替代品
  • Chris Liscio 分享了 Capo 应用内置 DSL 的设计:用于描述键盘与 MIDI 绑定,基于 Point-Free 的 swift-parsing 库构建
  • Nicolas Zinovieff 则展示了一个 符号数学 DSL 的实验:通过协议与运算符重载,让 (1 + 2 * "X") * (3 - "Y")成为合法的 Swift 表达式,并在提供具体值时惰性求值,核心实现不超过 300 行

在 macOS 菜单中清晰显示当前选中状态 (Indicating Selection in macOS Menus Using SwiftUI)

SwiftUI 提供了不少用于表达选择的组件,例如 PickerToggleMenu,但如何清晰地引导用户进行选择,并准确标识当前选中项,并没有想象中那样理所当然。Gabriel Theodoropoulos 从最基础的 Button 出发,一步步演进到 PickerToggle,系统梳理了几种常见方案及其各自的局限。文章的价值不在于给出“唯一正确解”,而在于提醒开发者:SwiftUI 提供的标准组件,并不会自动带来最佳的用户呈现效果。很多时候,你仍需在“系统一致性”与“实现自由度”之间做出权衡。


构建 List 的替代组件 (Building List replacement in SwiftUI)

如何在 ListScrollView + LazyStack 之间做出选择,一直是困扰不少 SwiftUI 开发者的问题。在本文中,Majid Jabrayilov 在重构其 CardioBot 应用时,选择基于 SwiftUI 的 Container View API(iOS 17+)构建了三个可复用的 UI 组件:ScrollingSurface、DividedCard、SectionedSurface,以替代 List。这些组件在使用方式上与 List + Section 高度相似,但彻底摆脱了 listRowBackgroundlistItemTint 等仅在 List 中生效的限制。

List 并非只是“有默认样式的 LazyVStack”——两者在底层架构、滚动控制、与导航容器的协同、以及大数据集下的性能表现上均有本质差异。如何在两者之间做出综合判断,可以参考这篇旧文。


AppIntents meet MCP

当大家还在将 AppIntents 视为 Siri 与快捷指令的“配套工具”时,Florian Schweizer 给出了一个更值得关注的方向:将 AppIntents 直接暴露为 MCP(Model Context Protocol)工具,从而让 LLM 能够调用你的应用能力。Florian 基于 SwiftMCP,通过宏构建 MCP Server,并将 AppIntents 无缝映射为 MCP Tools,使 AI Agent 能够直接调用应用中的 Intent,实现跨应用自动化。

去年便有传闻,苹果正在为其生态系统引入 MCP 支持。或许再过两个月,在 WWDC 26 上答案便会揭晓。


创建交互式小组件 (Ride the Lightning Air: Building Interactive WidgetKit Widgets)

很多开发者都会被 WidgetKit 的文档误导,错将 AppIntentTimelineProvider 视为实现交互按钮的关键——实则不然。它是为“用户可配置 Widget”(如长按后编辑设置项)准备的,与交互行为并无直接关系。而真正用于实现交互(按钮点击触发行为)的,依然是最基础的 TimelineProviderWesley Matlock 通过一个虚构航空公司的登机状态 Widget,完整演示了正确路径:使用 TimelineProvider + Button(intent:) + AppGroup 共享存储来构建交互式 Widget。

整个数据流形成一个清晰的闭环:用户操作 → Intent 执行 → 状态写入 → Widget 刷新 → UI 更新。


文件存储与 iCloud:从本地到云端的完整认知

在 iOS / macOS 开发和使用中,文件存储往往被当作“基础能力”,但它实际上直接决定了数据的生命周期与系统行为。

Working with files and directories in iOS 一文中,Natascha Fadeeva 系统梳理了 App Sandbox 的结构,以及 DocumentsLibraryCaches 等目录的职责划分,帮助开发者建立“什么数据该放在哪里”的基本认知,并说明如何避免无关文件被 iCloud 备份。

Howard Oakley 撰写的 Understanding and Testing iCloud 则从系统层面揭示了这些数据的“后续命运”。iCloud 并非单一服务,而是由 CloudKit、iCloud Drive、系统更新等多个子系统组成,不同数据类型对应不同的同步与备份路径。

文件的存储位置,并不仅仅是组织问题,更是在定义它是否会被备份、同步,以及在设备之间如何流转。

因此,iCloud 问题往往不是“是否开启同步”这么简单,而是涉及客户端、网络、缓存以及服务端限流等多个环节。

工具

Bad Dock: 让你的 Dock 图标动起来

这是一个“离谱但严肃”的 macOS 实验项目。Eric Martz 利用公开的 NSDockTile / NSDockTilePlugin API,绕过 Big Sur 之后 Dock 图标的 squircle 限制,将视频流直接渲染进 Dock 图标。实现思路并不复杂,但非常完整:使用 AVAssetReader 解码视频、主动降帧至约 12fps、通过 ring buffer 控制内存占用,最终将一个“整活想法”打磨成了结构清晰的技术验证。

这类项目的价值不在功能本身,而在于展示:系统 API 的边界,往往比文档写得更远。

补充说明:该项目实现的是运行时动态 Dock 图标(应用运行时持续绘制);应用退出后,仅能通过 NSDockTilePlugin 保留静态自定义图标。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

Harness Engineering: 让 Coding Agent 可靠完成长程任务

作者 百度Geek说
2026年4月9日 17:36

导读 introduction

Coding Agent 处理目标明确、规模可控的任务很成熟,但面对上千文件的批量迁移任务,会遇到上下文耗尽、中断无法恢复、规模放大后行为不可控等问题。本文从实际落地经验出发,提出任务拆解、并行执行、File As Progress 状态持久化、多层重试等核心设计,并结合真实场景展示完整方案。最终将这套编排经验沉淀为 meta-skill,让 Agent 自己生产长程任务的执行框架。

01 长程任务的特征

最近 Harness 这个词在 AI Coding 圈子里被频繁提起,Harness 的英文本意是缰绳,能让马往对的方向跑。放到AI Agent场景,就是模型能力很强,但需要一个工具使其能够在安全边界内被稳定地约束、引导和复用。

Coding Agent 已经能很好地处理目标明确、规模可控的任务了。但在做工程化建设的时候,有一类任务远比这复杂。比如:把 21 个前端模块的 JS 文件全部迁移到 TypeScript。对几十个模块做一轮全量 Code Review,再批量修复产出的几十上百条意见。把散落在代码各处的中文硬编码全部提取成 i18n 资源。

这类任务有三个共同特征:规模大,涉及成百上千个文件;运行时间长,一次跑不完,可能需要跨越多个会话;消耗 Token 极高,动辄几千万到上亿 Token的量级。

这就是我们说的"长程任务"。这不是什么新概念,我们在使用Agent完成较大规模的任务是很常见的。这篇文章往深处走一步,聊聊长程任务的 Harness Engineering。

02 关注的点

使用Agent完成任务,我们要关注下面的点:

效果。

任务能否完成:Agent在执行大量任务的时候可能在执行过程中中断,原因可能是Token超限、网络异常、服务中断等原因,任务停在半路。

完成的真实性:Agent 有时候并没有处理完所有文件,但它会告诉你"任务已完成",核查后才能发现。

中断后的连续性:断了之后能不能接上继续执行,接上之后的任务的执行质量会不会变差?

结果的可验证性:如何判断Agent执行的结果是正确的,当产出涉及几百上千个文件的变更,靠人工很难看过来,需要有程序化的手段去批量校验正确性。

速度。

1000 个文件逐个串行处理,即使每个文件只要 30 秒,也需要 8 个多小时。如果能 10 路并发,可能在 1 小时内搞定。

成本。

Agent 一次没做对,整个上下文的 Token 就浪费了。一个任务反复重试 3 次,成本直接翻 3 倍。更隐蔽的浪费是:Agent 在一个长会话里在长会话中逐渐偏离预期,前面消耗的几十万 Token 全部白费。

效果、速度、成本,这三者构成了长程任务的核心关注点。接下来所有的设计,都是围绕这三个目标展开的。

03 困难在哪里

从长程任务的特征出发,能推导出有三个核心困难点:

上下文耗尽。 模型的上下文窗口是有限的。当处理的文件越来越多,历史信息不断累积,上下文逐渐被填满。现在的 Agent 框架普遍带有上下文压缩能力,当上下文接近窗口上限时,自动对历史对话做摘要压缩。但压缩一定会丢失信息,每压缩一轮,前面的细节就模糊一层。随着任务推进、压缩不断叠加,即便是 Opus 这样的顶尖模型,对早期上下文的理解质量也会持续下降。你会看到 Agent 在第 50 个文件时"忘了"第 10 个文件建立的约定,或者重复犯前面已经纠正过的错误。更麻烦的是,模型在长上下文中还会出现"上下文焦虑":它感知到上下文快到上限了,就开始提前收尾、草草了事。Agent 明明还有文件没处理,却自己宣布"任务完成"。

中断要重来。 网络断开、Token 用尽、模型超时,这些不是异常,是常态。而 Agent 没有跨会话记忆。每次新对话开始,它面对的是一张白纸。如果没有任何恢复机制,中断就意味着从头再来,这不仅浪费已完成的工作,也拖慢了最终完成的速度,甚至可能进入“永远无法达到终点”的尴尬境地。

规模大了行为不可控。 单个文件做得好,不代表一千个文件都做得好。规模放大后,个别文件处理失败、输出格式不一致、生成的代码破坏构建,这些都会发生。如果一个文件的失败导致整个任务挂掉,那这个流程就不可能在生产环境中使用。

04 核心原则

要解决这些困难,需要下面四个原则。

任务拆解。 对应上下文耗尽的问题。不把所有事情塞进一个会话,而是把大任务拆成合理粒度的子任务,每个子任务是一个 Agent 能在单次会话内独立完成的工作单元。拆完之后,Agent 每次只需要关注有限的几个文件,上下文里只有当前任务需要的信息,不会被无关内容干扰。

并行执行。 对应速度的问题。拆完的几十上百个子任务,如果还是逐个执行,速度没有本质提升。必须支持多个 Agent 同时跑不同的子任务。

可续传。 对应中断要重来的问题。任务的进度必须持久化到会话之外的地方,使得任何一次中断后,新的会话都能接着上次的进度继续,而不是从零开始。

有完成条件。 对应行为不可控的问题。每个子任务必须有明确的、可程序化检查的成功标准。通过客观手段验证产出确实符合预期,如果不符合预期,给Agent失败的原因在当前的session继续修复,设定修复的轮次以及明确的停止边界,比如修复完成或者触发边界(比如Retry触发上限),标记FAILED。

任务拆解控制单次执行的复杂度,并行执行压缩整体耗时,可续传消除中断带来的沉没成本,完成条件保障产出的可信度。这些原则分别作用于效果、速度、成本三个维度。

05 理念

任务边界清晰。 每个子任务有明确的输入(处理哪些文件)、输出(产出什么、写到哪里)、约束(哪些操作绝对禁止)。子任务之间不共享状态、不交叉引用。这样做的好处是每个子任务可以独立理解、独立执行、独立验证。如果子任务之间有隐式依赖,一个任务的失败就会像多米诺骨牌一样影响其他任务。

根据任务间关系的不同,边界的实现方式分三种:

  • 无依赖,直接并行。 最简单的情况。子任务之间没有文件级别的依赖,各自处理各自的文件,互不干扰。比如做i18n 对每个文件的中文硬编码独立提取,不影响其他文件。这种情况下子任务之间不共享状态、不交叉引用,分组后直接并发执行。

  • 有依赖,拓扑排序。 子任务之间存在顺序约束,文件 A 依赖文件 B 的类型导出,B 必须先处理完,A 才能开始。JS to TS 迁移就是这种情况:被依赖的叶子文件先迁移,依赖它的文件后迁移。处理方式是在分组前先做依赖分析、按拓扑序排优先级,dispatch 时按优先级批次执行。同一优先级内的子任务仍然可以并行,但跨优先级必须串行。

  • 有冲突,物理隔离。 多个子任务可能修改同一个文件或互相影响的文件,比如 Code Review 修复时,两个 subAgent 可能同时改到同一个模块的公共配置文件。这种情况下让每个子任务在独立的 Git Worktree 中操作,各自修改互不干扰。冲突被推迟到合并阶段处理。当发生冲突时,直接使用脚本几乎不可能直接解决,代码层面的冲突往往需要理解上下文才能决定保留哪边,最终需要引入 Agent 来解决冲突。但延后处理的好处在于:所有子任务都已经执行完毕,工作区处于静止状态,Agent 面对的是一个确定的、不再变化的冲突集合,而不是在多个 Agent 同时修改的竞态中实时协调。在静止状态上解冲突,效果会好很多。只有当隔离方案确实行不通(比如子任务之间需要实时交换中间结果),才考虑 Agent Teams 这样的网状协作结构,但网状结构引入的通信开销和不确定性是显著的,因此是最后的选项。

图片

△ 不同任务边界的实现

错误在最小范围内解决。 "最小范围"是一个分层的概念。核心原则是:不将错误带到后续阶段,不将错误带出子任务。

  • 子任务内闭环。 子任务在执行过程中发现生成的代码编译不通过,应该在当前会话内尝试修复,而不是把编译错误留给后续步骤去处理。如果重试几轮仍然修不好,标记 FAILED、revert 环境,明确告诉上层"这里搞不定了"。不能让一个有问题的产出悄悄混进已完成的队列。

  • 阶段内收敛。 长程任务通常会分成几个阶段(比如"环境准备→批量执行→收尾验证")。如果子Agent的执行阶段发现了结果失败,应该在当前阶段内调整并重试,而不是带着问题进入收尾验证阶段再去补救。这样做的原因是:一旦子任务产出了一个有问题的文件,没被及时发现,下游任务基于这个文件继续工作,最后问题在验收时才暴露,导致浪费了整条链路的工作。所以阶段之间的交接必须是干净的:进入下一个阶段时,当前阶段的状态要么是"全部完成",要么是"部分完成、失败项已明确标记"。

步骤间有校验保障。 每完成一个子任务就立刻校验,不要攒到最后一次性验证。校验分两类:

  • 程序化校验:能用脚本判定的,绝不交给 Agent。**对于能用程序化验证的场景(比如 TypeScript 编译通过、成功完成构建、单元测试全部通过),这些都有明确的对错标准,用脚本自动检查,程序化校验的好处是零 Token 消耗、结果完全确定、可以无限次重复执行。

  • Evaluator 校验:需要主观判断的,用独立会话的 Agent 审核。**对于需要主观判断的场景(比如 Code Review 意见的质量),用独立的 Evaluator Agent 去审核。其中要注意的点:做事的 Agent 和评价的 Agent 必须在不同的会话进行,这是因为在同一个会话内,Agent 的历史推理过程会形成一种"自我说服"效应,影响后续的判断,让 Agent 倾向于认为自己之前的产出是正确的。打开一个全新的会话,Agent 面对的是干净的上下文,只看到产出本身,不受执行过程的干扰,得到客观的评价。甚至可以用不同的模型来做 Evaluator,比如用 Sonnet 模型进行 Code Review 的任务,再用 GPT 去做意见置信度判断(Grader),将置信结果交回给 Sonnet 去修复。跨模型的评估能引入不同的"视角",进一步降低偏见。

允许局部失败。 1000 个文件中有 5 个处理失败,不应该阻塞其他 995 个。任务编排框架要能容忍局部失败,已完成的部分照常合并产出。“失败”在实际实践中分为两种情况:

  • 确实搞不定,回退给人工。 子任务重试多次仍然无法通过校验,或者产出的结果明显错误。这种情况下 revert 工作区、标记 FAILED,留给人工处理。这是真正意义上的失败,不能强行合入。

  • 能搞定但不完美,接受有限的妥协。 比如 TS strict 迁移中,一个文件的绝大部分 any 都被正确消除了,但有一两处复杂的泛型推断 Agent 实在处理不了,留下了 // @ts-ignore 或者少量 any。编译能通过,业务逻辑没被改坏,只是没有达到 100% strict 的理想状态。这种情况可以标记为"通过但有妥协"(比如状态设为 DONE_WITH_WARNINGS),照常合入,把遗留的 any 记录下来作为后续人工优化的清单。如果把这种情况也当作硬失败来处理,revert 整个文件、标记 FAILED,导致大量文件在"99% 搞定"的状态下被反复重跑,浪费 Token。

06 技巧

在建设了大量的Long Term Task Skill实践后,总结出下面的技巧:

6.1 任务粒度

拆解的第一个问题是:拆到多细?

粒度太粗,单个子任务塞进去的文件太多,上下文又开始膨胀,回到了老问题。粒度太细,每个子任务只处理一个小文件,调度开销和 Prompt 模板本身的 Token 消耗占比过高,效率反而下降。

合理的粒度取决于三个因素:模型的上下文窗口大小、单个文件的平均规模、任务本身的推理复杂度。

我们在实践中用了一个经验公式来估算。以 JS to TS 迁移任务为例:

模型使用 Claude Sonnet,有效上下文窗口大约 200K Token。一个子任务的 Token 消耗由几部分组成:Prompt 模板(任务说明、规则约束、输出格式要求)大约 1K Token;输入文件内容,代码文件大约每行 10-20 Token,3000 行代码约 30K-60K Token;Agent 的工作过程(读文件、推理、写代码、跑验证、修复错误),这部分消耗通常是输入的 2-3 倍,因为 Agent 不是一次性处理完的,它会多轮读写文件、执行命令、检查结果,每一轮都会累积上下文,大约 60K-180K Token。加起来大约 90K-240K Token。所以 3000 行是一个让单次子任务能在上下文窗口内完成的经验上限,给多轮交互和可能的修复留有余量。但 3000 行不是一个写死的常数。不同任务的推理密度不一样:比如当需要 Agent 深入理解代码意图、评估设计合理性,推理消耗远大于输入本身,3000 行可能就偏多了。

判断粒度是否合适,有一个简单的检验标准:跑几组样本,看子任务的 Token 消耗是否经常逼近上下文窗口的 80%。如果经常逼近,说明粒度偏粗,应该缩小;如果只用到了 30%-40%,说明可以适当放大,减少调度开销。

还有一个容易忽略的点:同目录的文件应该尽量放在同一组。不是因为它们行数加起来刚好合适,而是因为同目录的文件往往共享 import、类型定义、配置文件。放在一起处理时 Agent 能看到完整的局部上下文,做出的修改更准确。

6.2 子任务的 CLI 化与并发调度

子任务不在 Agent 的对话里嵌套调用,而是作为独立的 CLI 进程执行。每个子任务是一次独立的 Agent 会话,由外部脚本启动和管理。这个设计选择带来了几个重要的好处:

Prompt 的确定性。 每个子任务的 Prompt 由 build-prompt.js 脚本根据任务参数程序化组装,包含明确的任务详情、规则约束、输入文件列表、输出格式要求、验证标准。所有子任务拿到的指令结构一致,不会因为主 Agent 在长会话中的"自由发挥"导致子任务理解偏差。

比如没有程序化构建 Prompt 时发生过这样的问题:你给主 Agent 的指令是这样的:

使用 subAgent 完成 code review 任务,任务 Prompt 如下:
---
请审查以下文件,按 error/warn/style 三级分类产出审查意见。
待审查文件:src/components/UserCard.tsx, src/components/UserList.tsx, src/components/UserDetail.tsx
---


但主 Agent 并不会原样转发。它"理解"了任务后,实际传给 subAgent 的 Prompt 变成了这样:

你需要审查以下组件代码。重点关注边界情况和错误处理。
以下是文件内容:
// === src/components/UserCard.tsx ===
import React from 'react';
... (200行代码被直接贴入)
// === src/components/UserList.tsx ===
... (150行代码被直接贴入)
// === src/components/UserDetail.tsx ===
... (300行代码被直接贴入)
请按 error/warn/style 三级分类产出审查意见。


对比发现经过主Agent的转述内容变了,把文件内容全部贴进了 Prompt 而不是让 subagent 自己去读文件,subagent 失去了渐进式发现代码结构的机会,审查变成了"看一坨代码然后给意见"而非"逐个文件深入理解再评价";还塞了属于主 Agent 自己推断的上下文,万一推断有误,subAgent 的审查方向就被带偏了。最终导致审查效果打折扣。

Token 消耗大幅降低。消除上下文累积,每个 CLI 子任务是一个全新的 Agent 会话,上下文里只有当前子任务需要的信息。如果在主 Agent 的会话中串行调度,到第 30 个子任务时,前面 29 个子任务的对话历史全部堆积在上下文中,白白消耗 Token,还会干扰 Agent 的注意力。 同时也可以减少Prompt 构建本身的消耗****,在主会话中,Agent 需要花 Token 去"想"怎么写子任务的指令,组织措辞、回忆约束条件、决定输出格式。build-prompt.js 脚本生成 Prompt 是程序化的省去这部分的Token。

并发数量可控。 CLI 进程可以由 dispatch.js 脚本自由控制并发数。资源充裕时开 10 路并发,资源紧张时降到 3 路,甚至可以根据 API 限流情况动态调整。在对话内让 Agent 自己调度并发,效果很难保证,模型往往过于谨慎,不愿意一次性开启几十、上百个并发子任务,很难真正达到高并发提速的效果。

可以通过脚本在前后插入逻辑。 子任务执行前,脚本可以做预处理(创建 Git Worktree、准备输入文件、检查前置条件);执行后,脚本可以做后处理(校验产出、更新状态、清理临时文件)。这些逻辑是确定性的,不需要 Agent 参与。

下面讲讲并发调度的具体设计。

dispatch 由两个脚本分阶段协作:dispatch.js 负责首批启动,poll.js 负责后续监控和补位。

dispatch.js 只执行一次。它读取任务清单,为前 N 个任务(N = 并发上限)完成前置准备和任务分配的的工作,比如创建 Git Worktree、生成 Prompt、启动子 Agent 进程,剩余任务标记为 pending 等待补位。以 Code Review 修复为例,每个任务启动前脚本先 git worktree add 创建隔离工作区,然后内联的 generateQuery 函数根据任务参数(文件路径、review 意见列表、分支名)程序化组装 Prompt,最后 spawn 子 Agent 进程在 Worktree 中执行。启动完成后,dispatch.js 将每个任务的状态(running/pending/failed)、PID、启动时间写回任务清单文件,然后退出。

poll.js 由主 Agent 在循环中反复调用。它是单次执行的,主 Agent 每隔一段时间调用一次,检查退出码决定是否继续。每次调用时 poll.js 做三件事:

第一,检查所有 running 状态的任务。通过 PID 判断进程是否还活着,如果进程已退出,去 Worktree 里找 fix_result.json,存在且合法就标记 success、备份结果;不存在就标记 failed、从日志末尾截取错误信息。

第二,补位启动 pending 任务。统计当前 running 的数量,如果低于并发上限,就从 pending 队列中取出任务,创建 Worktree、生成 Prompt、启动子 Agent,填满并发槽位。

第三,输出状态摘要并通过退出码传递信号。exit 0 表示还有活跃任务(pending 或 running),主 Agent 应该 sleep 后继续调用 poll.js;exit 2 表示所有任务已到终态,主 Agent 可以进入下一阶段(比如合并结果)。

主 Agent 的调度循环非常简单:

while true; do
    node scripts/poll.js --task-list task_list.json
    if [ $? -eq 2 ]; then break; fi
    sleep 60
done


整个调度过程中,Agent 只负责"审查代码并给出意见"这一步,其余的 Worktree 管理、Prompt 构建、进程监控、状态更新、补位启动全部由脚本完成。

图片

△ dispatch调度架构图

为 dispatch 接口的参数进行统一设计:--root(项目目录)、--concurrency(并发数)、--dry-run(预览模式)、--retry-failed(重试失败任务),这样所有长程任务 Skill 的调度方式都是一致的。

这套设计解决了几个问题。

调度策略:随到随补,不等齐再发。 不用等当前批次的所有任务都完成才启动下一批,而是哪个坑位空了就立刻补上新任务。因为子任务的执行时间不均匀——一个只有 3 个小文件的目录可能 20 秒就审完了,一个有复杂业务逻辑的目录可能要 3 分钟。如果按批次等齐再发,假设一批 10 个任务中 9 个在 30 秒内完成、1 个要 3 分钟,那 9 个坑位会空等 2 分半,十几批下来累计浪费的时间相当可观。随到随补策略让每个坑位始终有任务在跑,整体吞吐量最大化。

输出设计:对人和对 Agent 分两个通道。 设想一个具体的场景:你启动了 120 个 Code Review 子任务,10 路并发,预计 40 分钟跑完。你在终端前盯着输出,想知道"现在跑到哪了、有没有异常、还要多久"。半小时后进程意外中断了,一个新的 Agent 会话启动,它需要知道"哪些任务完成了、哪些失败了、从哪里继续"。

这两个受众的需求完全不同。作为工程师,需要的是一眼能扫到的进度概览,数字、比例、异常摘要。不需要看到 120 行的完整任务清单,那反而是噪音。Agent 恢复时需要的是结构化的、可程序化解析的完整状态数据,每个任务的 ID、状态、产出路径、失败原因。

所以终端输出给人看,简洁、可扫视:

[Progress] 45/120 DONE | 10 IN_PROGRESS | 3 FAILED | 62 TODO
[Speed] avg 35s/task | elapsed 28min | ETA ~22min
[Failed] group_12 (timeout), group_27 (compile error), group_33 (timeout)


结构化的完整状态写到文件里给 Agent 读,这是 File As Progress 的状态文件。dispatch 脚本不需要在终端输出里重复这些信息,Agent 恢复时直接读文件。

6.3 File As Progress

这是长程任务编排中最核心的设计。

所有进度状态持久化到文件系统。不依赖 Agent 的记忆,不依赖会话上下文,只依赖磁盘上的文件。每完成一步操作,立即写入文件。不攒批,因为 Agent 随时可能被中断。

这意味着无论 Agent 在哪一步被打断,下次启动时只需要读文件就能知道"上次跑到哪了"。新会话开始时,Agent 不需要任何历史上下文,它只需要:读任务清单文件,看哪些是已完成的、哪些还没开始、哪些失败了需要重试,然后从断点继续。

文件载体可以是 TSV(简单任务,人可读)、JSON(复杂任务,支持嵌套元数据),甚至纯文本。形式不重要,重要的是"一切状态都在文件里"这个约束。

6.4 任务状态设计

有了 File As Progress,还需要一套状态机来描述每个子任务的生命周期:

TODO → IN_PROGRESS → DONE
                   → FAILED
                   → SKIPPED


状态设计的核心理念是:仅凭当前状态就能决定下一步做什么。恢复逻辑不需要知道"之前发生了什么",不需要回放历史,只需要读到"当前状态是什么",就能推导出续传策略。这要求每个状态都是自描述的,它本身就携带了"接下来该怎么办"的信息。

基于这个理念,可以根据任务的复杂度设计更精细的状态,实现更精确的续传。比如一个有"分析→执行→校验"三步的子任务,粗粒度状态只有 TODO/DONE/FAILED,中断在"执行"阶段时只能从头重来。如果细化为:

TODO → ANALYZING → ANALYZED → EXECUTING → EXECUTED → VERIFYING → DONE
                                                               → FAILED


那恢复时可以精确判断:状态停在 ANALYZED,说明分析已经完成,直接从执行阶段开始;停在 EXECUTED,说明执行完了但没来得及校验,直接跑校验。每多一个状态,就多一个可以精确恢复的断点,减少重复工作。****细粒度状态必须搭配对应的持久化产物,本质还是File As Progress的思路,****每个中间状态都需要对应一个落盘的文件,例如ANALYZE将结果写入到文件,EXECUTING才能立即恢复回来。

但状态也不是越多越好。每个状态都需要对应一个持久化的检查点(比如一个中间文件或一条状态记录),维护成本不低。实际设计时,建议在任务执行时间较长、或者某个步骤有明确的中间产物可以复用的地方增加状态。对于 10 秒就能跑完的子任务,TODO/DONE/FAILED 三个状态就足够了,即使中断了,重跑的成本也很低。

最简单的恢复逻辑是:找到所有 TODO 和 FAILED 的任务,继续执行,已经 DONE 的跳过。

但有一个细节需要特别注意:IN_PROGRESS 状态的残留处理。如果 Agent 在执行某个任务的过程中被中断,这条任务的状态会停留在 IN_PROGRESS,但实际上没有任何 Agent 在处理它。恢复时必须判断这个 IN_PROGRESS 到底是"真的在跑"还是"跑到一半挂了"。

判断的依据是产出物而非状态本身。具体分几步:

第一步,检查是否有预期的产出文件存在。每个子任务在开始时就应该明确"完成后会产出什么文件",比如 TS 迁移任务会产出 .ts 文件,Code Review 任务会产出 .review.json 文件。

第二步,如果产出文件存在,检查内容是否合法。不是简单的"文件非空"就行,而是要做完整性校验:TS 文件能不能通过编译、JSON 文件能不能解析、Review 意见的格式是否符合 schema。如果校验通过,说明 Agent 其实做完了,只是状态没来得及更新。直接将状态更新为 DONE。

第三步,如果产出文件不存在,或者内容不合法,说明这是半成品。这时需要清理工作区。清理的关键是"能找到哪些是半成品"。我们的做法是:每个子任务在独立的 Git Worktree 中操作,所有修改都发生在这个隔离的工作区里。判断半成品的方法是:在 Worktree 中执行 git statusgit diff,所有未提交的变更就是半成品。清理更为简单,git worktree remove 丢掉整个 Worktree 目录,工作区回到执行前的干净状态。如果没有使用 Worktree,而是在主分支上操作,那就需要在任务开始前记录当前的 commit hash,半成品清理时 git checkout <hash> -- <files> 恢复。

完成清理后,把状态重置为 TODO,等待下次调度。

6.5 多轮重试

subagent 失败是正常的。超时、输出格式错误、生成的代码无法编译,这些都会发生。将失败进行分层,不同层次对应不同的重试策略。

内层:恢复会话。 子 Agent 因为网络异常、进程崩溃、compress 错误等原因异常退出,任务本身没有错。dispatch 脚本记录了这次执行的 conversationId,检测到异常退出后,恢复同一个会话说"继续",Agent 从断点接着跑。这相当于续传,降低了成本。比如 6 个文件改了 4 个时进程崩溃,恢复同一个会话后 Agent 直接从第 5 个继续。

中层:带反馈重试。 子 Agent 正常完成了,但产出不满足校验条件。开一个新的子 Agent 会话,把错误信息作为上下文喂进去,针对性修复。比如 tsc --strict 报了 3 个类型错误,把完整报错信息带给新会话,Agent 只修这 3 处,不重新改造整组。限制 2-3 次。达到上限仍然失败,revert 工作区、清理环境、标记 FAILED。到此为止,子任务层面不再做更多尝试。

外层:主 Agent 重新调度。 中层耗尽后留下了一批 FAILED 的文件。要不要对这些文件重新 dispatch 给子 Agent 再来一轮?这需要在主 Agent 层面去决策。重新 dispatch 意味着为这些文件重新分组、生成新的 Prompt、启动新的子 Agent,相当于把它们当作全新的子任务再跑一遍。这里需要权衡成本和时间:如果 FAILED 的文件只有两三个,重新 dispatch 一轮的代价不高,值得尝试;如果 FAILED 了几十个,可能说明任务规则本身有问题,盲目重跑只是浪费 Token,不如先排查原因再决定。

判断走哪层,看产出文件的状态,不依赖解析 Agent 的文本输出。文件是否存在、内容是否合法,是稳定的、可程序化检查的依据。

图片

△ 多轮重试分层

07 示例

上面的内容是从抽象的层面去介绍存在的困难点以及如何去解决。下面用两个真实落地的场景来具体展示它们如何组合成完整的执行方案。

7.1 全量 Code Review

场景: 21 个前端模块做一轮全量代码审查,产出审查意见并批量修复。

任务拆解: 按模块分组,每个模块内按目录归类文件,单文件源码行数超过 MAX_LINES(默认 500) 则独立成组。分组时同目录的文件放在一起,因为它们往往有共享的 import 和类型依赖,放在一起审查质量更高。

其中MAX_LINES 的选取逻辑:每个 chunk 的 token 构成为:源文件正文(主要消耗):agent 执行时从磁盘读入源文件,平均 1 行 ≈ 14 tokens(按 50 字符/行、3.5 字符/token 估算)

固定开销:prompt 模板、规则、文件元信息、review 输出及 agent 中间步骤,约 8,000 tokens

以 500 源码行为例,单 chunk 总消耗约 20,000 tokens,占 Claude Sonnet(200K)窗口的 10%,为并发子任务的对话历史和输出留出充足余量。

并行执行:dispatch 脚本以 CLI 方式启动多路 subAgent 并发审查,每个 subagent 读取自己对应的inputs/{chunkId}-input.json,审查完毕后直接将 issue 列表写入segments/{chunkId}.json。主 Agent 通过检查 segment 文件是否存在来判断任务是否完成。

校验保障: 审查意见的质量属于主观维度,需要独立的 Evaluator Agent 校验。架构变成三个角色:主 Agent 编排、subAgent 执行审查和修复、Evaluator Agent 对意见做批判性评估。Evaluator 的 Prompt 要带有挑战性语气,主动挑毛病而非寻找优点。

续传: 审查进度写入 TSV 文件。中断后恢复时,读取 TSV 文件定位到当前 Phase,跳过已完成的模块,继续处理未完成的。

7.2 JS to TS 迁移

场景: 将整个项目的 JavaScript/JSX 文件批量迁移到 TypeScript/TSX。

任务拆解: 按同目录归组,累计行数不超过 3000 行,单文件超过上限时独立成组。但这个任务有一个额外的约束:文件间存在依赖关系。被依赖的叶子文件必须先迁移完成,依赖它的文件才能开始。所以分组时还需要按依赖拓扑序排优先级。

完成条件: 这个场景可以完全用程序化验证。每个文件迁移完后,用 Babel AST 对比确保逻辑结构不变,用 tsc 做类型检查确保编译通过。两项都通过才标记为 DONE。

错误隔离: 某个文件迁移失败,不影响其他文件。失败的文件保留原始的 JS 版本,状态标记为 FAILED。整个项目在部分文件未迁移的情况下依然可以正常构建(因为 TypeScript 项目可以混用 JS 和 TS)。

File As Progress:migration-tasks.tsv 记录每个文件的迁移状态。每次启动按顺序检查:.agent.env 存在且含 token → migration-tasks.tsv 已生成 → 无 IN_PROGRESS 残留 → 进度是否全部完成,从第一个不满足的条件对应的 Phase 继续执行。每个 Phase 对应独立的 reference 文件,Agent 只读当前 Phase 所需的指令。

08 从经验到框架:Skill for Skill

在实现了上述一系列长程任务后,我们发现每个任务的骨架结构是高度一致的:

<skill-name>/
├── SKILL.md                    # Phase 定义 + 会话恢复检测 + 完成标准
├── scripts/
│   ├── discover.js             # 扫描目标,生成任务清单(幂等)
│   ├── dispatch.js             # 读清单,分组,并发调度 subagent
│   ├── build-prompt.js         # 程序化构建子任务 Prompt
│   ├── poll.js                 # 轮询子任务状态 + 补位启动
│   ├── merge.js                # 收集子任务结果,合并为最终产物
│   └── status.js               # 查询整体进度
├── references/
│   ├── phase0_setup.md         # 环境配置指令
│   ├── phase1_analyze.md       # 分析规划指令
│   ├── phase2_dispatch.md      # 批量执行指令
│   └── phase3_finalize.md      # 收尾验证指令
└── evals/
    └── evals.json              # 评估用例


这就引出了一个更进一步的思路:能不能把长程任务的编排经验本身也做成一个 Skill?

我们做了一个叫 long-term-task-orchestration 的 meta-skill(元技能)。它不直接执行任何业务任务,而是教 Agent 如何创建新的长程任务 Skill。当你告诉 Agent"我需要一个批量做 X 的 Skill",它会读取这个元技能的 reference 文件,按照模板生成完整的 SKILL.md、scripts 目录、references 目录,自动包含 Phase 设计、状态管理、并发调度、恢复逻辑。

这是用 Agent 来强化 Agent 的工作能力。不是让 Agent 做一次任务,而是让 Agent 生产出能反复做这类任务的工具,节省每一次长程任务都需要工程师亲自编排的成本。

可以从仓库地址(github.com/hixuanxuan/… Code环境下安装,安装命令如下,会将long-term-task-orchestrationskill-eval(用于评测)一并安装。

npx skills add hixuanxuan/long-running-agent-tasks -y


long-term-task-orchestration 配合 skill-creator 一起使用。你只需要用自然语言描述任务目标,Agent 会自动完成从骨架生成到脚本填充的全过程。示例prompt:

/long-term-task-orchestration 创建skill实现React Compiler迁移并下线全部memo。


Agent 会自动生成完整的 Phase 设计、脚本目录、状态管理和恢复逻辑,并自动启动skill-eval评测,将 body 的评测结果可视化展示给用户并询问是否需要继续,确认后启动循环修复的过程,确保Skill的 workflow 是可以稳定执行的。工程师不需要关心背后的并发调度、断点续传、重试分层这些编排细节。当出现任务类型是对大量同类目标执行相同的操作,并逐个验证结果,可以使用这个这个meta-skill帮助你快速完成Skill的创建,并使用创建的Skill在项目中高效稳定的完成任务。

09 结尾

Harness 中的每一个环节,都隐含了一个"当前模型做不到"的假设。随着模型能力提升,这些假设会逐渐过期。

做Harness Engineering 是在模型能力和工程可靠性之间找到合适的边界。模型每一次进化,这个边界都会移动:曾经需要脚本控制的环节,可能下一代模型就能自主处理了。但"确定哪些环节该交给模型、哪些该留在框架里"这个判断本身,不会因为模型变强而消失。每当新模型出现,重新审视这个边界,去掉一个环节,观察对结果的影响。

Harness Engineering 是团队基础设施建设的一部分,解决 Agent 完成大规模任务时的不确定性,并提供可量化的结果评估能力。

目前中国大陆唯一可以免费在 Xcode 中使用顶级大模型智能编程的方法

2026年4月7日 09:52

在这里插入图片描述

0.引子

现今,在中国大陆想要使用最强编程大模型在 Xcode 中实时交互的方法不多。

为了体验 Vibe Coding 的“畅快”打击感(或许还有等待间隙时的些许失落感),我们往往需要在 Cursor 和 Xcode 间无限切换,这多少有点让秃头小码农们有些不爽快!

在这里插入图片描述

况且第三方智能编程 IDE 与 Xcode 联合开发还有一个问题:就是从 Xcode 外部无法精确的感知和处理 Xcode 中的细枝末节。举个例子:宝子们见过 Cursor 为了修复 1 个 bug 却新产出 10 个 bug 的蛋疼壮观场面吗?

在这里插入图片描述

幸运的是,在 Xcode 最新正式版 26.4 中: 在这里插入图片描述

我们找到一种免费且非常简单就可以辅以超强编程大模型(gpt-5.4 或 gpt-5.3-codex 家族)的方法:

在这里插入图片描述

操作起来也非常简单,目前(2026.4.7号)并不需要付费 OpenAI 账号或绑定任何国际银行卡。

在这里插入图片描述

这样宝子们“足不出户”就可以在 Xcode 里享受氛围编程的乐趣了哦。

在这里插入图片描述

废话少叙,心动不如行动!

让我们马上开始操练起来,将 Xcode 打造为丝毫不输于 Cursor 的智能 IDE 吧!8-)


1.工欲善其事,必先利其器

首先,大家需要下载和安装 Xcode 26.4 正式版。

同时,必须保证我们可以访问到 ChatGPT 官网,否则还扯什么呢?

在这里插入图片描述

2.启用 Xcode 智能 Agent

运行 Xcode ,打开设置,进入 Intelligence 页面:

在这里插入图片描述

Xcode 26.4 支持先进最强的 2 个编程大模型智能体(Agents):ChatGPT Codex 和 Claude,不过目前后者在大陆无法登录,会提示:当前区域的服务不可用。

在这里插入图片描述

所以,我们只有“稍微”退而求其次一丢丢,来使用 gpt-codex 了。

点击 Codex 右侧的 Get 按钮,下载并安装 Agent 到本地,我们能看到只有 77MB,可谓相当“小鸟依人”:

在这里插入图片描述

接下来的一步就是进入 Codex 智能体(Agent)页面,登录 ChatGPT 账户即可:

在这里插入图片描述

如图所示,在登录了 gpt 账号之后,我们可以就可以恣意选择自己喜爱的 gpt 大模型啦:

在这里插入图片描述

不过据我观察,Xcode 智能 Agent 中的 gpt 编程大模型貌似有点缩水,少了不少强力模型哦(比如 GPT-5.3 Codex High 和 GPT-5.3 Codex Extra High 等):

在这里插入图片描述

但话又说回来,对于这免费的“飞来横福”,我们还要什么自行车呢?


注意:正如之前所说的,目前只需免费的 ChatGPT 账号即可,且不需要绑定任何银行卡。

但是,未来还能不能享用这“免费的午餐”,就有点世事难料了。


在这里插入图片描述

3. 测试

在上面各步骤都就绪之后,我们就可以找一个项目实际在 Xcode 中小试身手了。

下面,打开宝子们最爱的项目,先让 Xcode Agent 为我们总结一番吧:

在这里插入图片描述

当然,在 Xcode 里编程智能体做的不仅仅是做个总结那么“弱智”,我们还可以让它直接分析 Xcode 中拥有的一切:

在这里插入图片描述

现在,直接在 Xcode 中用 AI 来修正编译错误不再是梦想了:

在这里插入图片描述在这里插入图片描述

这样做可以最大化利用 Xcode 丰富的上下文来让 AI 充分考虑和修正问题,避免了外部智能 IDE(比如 Cursor、Qoder 等)无必要的切换和折腾。


想用 Xcode 与本地大模型“双剑合璧”来协同编程的宝子们,请移步如下链接观赏精彩的内容:


看到这,不知宝子们心动了吗?

在这里插入图片描述

要不要一起来借助 Coding Intelligence 来试试 Xcode 的氛围编程呢?8-)

若有任何与本文相关的配置问题,请宝子们毫不犹豫的私我哦!

感谢观赏,下次再会吧!

在这里插入图片描述

苹果的罕见妥协:当高危漏洞遇上“拒升”潮 -- 肘子的 Swift 周报 #130

作者 东坡肘子
2026年4月7日 07:53

issue130.webp

苹果的罕见妥协:当高危漏洞遇上“拒升”潮

对于 iOS 用户来说,最近或多或少都会看到与 Coruna、DarkSword 有关的高危漏洞消息。两个攻击链均采用水坑攻击的方式,攻击者无需受害者进行任何交互,仅需访问一个被植入恶意 iframe 的合法网站或加载恶意广告,即可触发完整的攻击链,在窃取资料后自动清理攻击痕迹。由于工具链利用的漏洞存在于 iOS 13 至 18.7 的绝大多数版本中,截至目前,已有上亿用户受到影响。

Coruna 主要针对 iOS 13 至 iOS 17 的设备,在过去几个月间,苹果已为这些系统推送了多次安全更新。DarkSword 则主要针对 iOS 18.4 至 18.7 的设备。尽管这部分设备均具备升级至 iOS 26 的硬件条件,但由于种种原因,仍有不少 iOS 18 用户选择按兵不动。

在很长一段时间里,苹果用户对于系统更新的态度都相当积极,这也是苹果生态的一大特色。但这一趋势在去年出现了变化——Liquid Glass 带来的巨大视觉冲击,让苹果用户中第一次出现了相当比例主动拒绝升级到 iOS 26 的现象。与此同时,为遵守英国《网络安全法》(Online Safety Act)的要求,苹果在 iOS 26.4 中为英国用户引入了强制年龄验证机制,由于验证条件严苛,不少成年用户甚至被系统强行锁入‘儿童模式’,进一步推动了英国用户停留在 iOS 18 或 iOS 26.3 的风潮。而拒绝安装新版本,意味着这部分用户同时放弃了后续所有安全补丁,让设备进一步暴露在潜在风险之下。

面对这一局面,苹果承受了明显的舆论压力与品牌风险。特别是在 3 月下旬,DarkSword 的完整攻击代码被泄露到了 GitHub 上,让这一国家级黑客工具瞬间平民化,直接迫使苹果必须采取紧急行动。最终,苹果罕见地为 iOS 18 单独推出了安全补丁 iOS 18.7.7,将原本仅用于 iOS 26 的防护机制回移植到旧系统。至此,苹果完成了针对本次高危漏洞的全部官方安全响应。

无论是苹果还是生态中的开发者,大多希望用户能积极跟进系统更新——既能减少多版本适配的维护负担,也能让用户尽快享受到新 API 带来的便利。但现实是,始终有一部分用户出于性能、续航、使用习惯乃至隐私等方面的考量,有意将设备锁定在某个版本。

本次事件或许会带来两个方向上的变化:苹果在压力下调整了长期坚守的更新策略,为刻意留守旧系统的用户做出了妥协;而事件本身的广泛传播,也可能促使更多用户从安全角度重新审视“能不更新就不更新”的惯性,回到积极更新的轨道。这种双向的改变,或许正是这场风波意料之外的收获。

本期内容 | 前一期内容 | 全部周报列表

近期推荐

通过 Animatable 深入 SwiftUI 动画 (Animatable in SwiftUI Explained - Complete Guide with Examples & Deep Dive)

网络上并不缺少探讨 SwiftUI 动画机制的文章,但 Sagar Unagar的这篇仍然提供了一个颇具启发性的切入点。他没有从隐式或显式动画入手,而是围绕 Animatable 协议做了一次系统梳理:从 animatableData 的作用,到 AnimatablePair 如何承载多个插值参数,再到通过自定义 VectorArithmetic 让更复杂的数据结构参与动画。文章最值得注意的一点在于其核心视角:SwiftUI 实际上是在“动画数据”,而非直接对视图进行动画处理。


在 Swift Package 中共享本地化资源 (Localization in Swift Packages)

Xcode 能为 .xcstrings 文件自动生成类型安全的 Swift 符号,但这些符号仅在资源所在的 module 内可见——一旦将本地化资源抽离为独立的 Localization 包,其他 feature 包便无法享受编译期检查的优势。Khan Winter 的解决方案相当直接:通过一个 bash 脚本解析 .xcstrings 的 JSON 结构,生成 public extension LocalizedStringResource 扩展,使所有模块都能以 .l10n.helloWorld 的形式访问翻译键。

其中一个颇具参考价值的细节是 Debug 模式下的 @dynamicMemberLookup 设计——访问不存在的键时仅记录日志而不崩溃,而在 Release 构建中仍保留完整的编译期校验。相比基于 Swift 可执行文件的方案,这种实现更加轻量,复制脚本即可使用。


Coordinator 全局导航模式 (SwiftUI Coordinator Pattern: Navigation Without NavigationLink)

尽管 SwiftUI 一直在丰富基于状态驱动的导航 API,但管理全局导航一直是 SwiftUI 中的一个“痛点”。Wesley Matlock 以一个五 Tab 的音乐收藏应用为例,展示了如何通过 Coordinator 模式将导航决策从 View 中抽离:用一个 Route 枚举统一描述所有目的地,由单一的 Coordinator 对象持有导航状态并执行跳转,View 只需声明“去哪”而无需关心“怎么去”。文章没有回避 NavigationPath 不透明、路由携带模型对象导致的 Hashable 困境等实际问题。对于大多数中等规模的 SwiftUI 应用来说,这是一个务实且易于落地的导航治理方案。


把 Hacking with Swift 的编程风格写进 AI (Teach your AI to write Swift the Hacking with Swift way)

Paul Hudson 和他的 Hacking with Swift 让很多开发者走上了 Swift 与 SwiftUI 的学习之路。在 AI 时代,Paul 不仅推出了面向苹果开发生态的各类专业 Skill,也开始尝试在与 AI 的协作中注入更具个人特质的编程风格。

在本文中,他分享了一份极具辨识度(且充满他标志性幽默)的 AGENTS.md 配置。这套规则不仅约束了 AI 的技术选型,还为 AI 注入了 Paul 的灵魂:强调先展示结果再解释原理、偏好清晰而非炫技、甚至包括在代码写得漂亮时适时地喊出一句 "Boom!"。与其说这是一份用于 AI 的“系统提示词”,不如说是在为 AI 定义一种编码哲学——某种程度上,这种方式正在将冷冰冰的“代码生成”推向带有人情味的“风格迁移”。


AI Agent 的道与术

在刚过去的 Let's Vision 2026 中,王巍(Onevcat) 发表了关于在大型开发团队中应用 AI Agent 的演讲。整场分享讨论的重点,并不是某个具体工具有多强,而是当代码实现成本被迅速压低后,团队该如何重新组织开发流程,以及工程师的价值该如何重新定位。

作为 LINE 应用开发团队的一员,Onevcat 在过去几个月中的工作重心也已明显发生变化。用他自己的话说,他正在逐步从传统意义上的 iOS 工程师,转向探索如何将 AI 应用于服务大型产品研发团队的实践者。这种角色上的变化,也让这场分享比一般的工具介绍更有说服力。

演讲围绕三个关键问题展开:如何控制上下文污染,如何把个人经验沉淀为团队可复用的 memory 与 skill,以及如何让协作模式从“人指挥多个 Agent”逐步走向更自动化的闭环。里面有不少相当接地气的实践建议,例如将 AGENTS.md 控制在精简范围内、为 Agent 提供模块定位与架构速查脚本、鼓励 Claude Code、Codex、OpenCode 等多种 harness 并存,以及通过 webhook、cron、pipeline 和自动验收机制让 Agent 真正进入团队流程。

演讲稿仓库 中不仅包含完整的 Slidev 源码,也保留了不少演讲配套材料,包括原始资料收集和与 AI 协作的完整 trace,值得一并阅读。


从零开始:用 AI 开发一个 iOS 原生 APP 完整指南

我经常会在社交媒体上看到一些零基础的“开发者”通过 AI 构建了自己的产品或服务。尽管我使用 AI 的时间也不短,但我仍然比较困惑:这条路径真的像大家描述的那样有效吗?Zachary Zhang 分享了他完全借助 AI 工具,从零构建并上架一款纯原生 iOS 应用(SwiftUI + Cloudflare 后端)的实战全过程。这篇文章最让我印象深刻的,是他严谨的“工程化管线”:在让 AI 写代码前,必须先生成结构化的 PRD 和 HTML 格式的视觉参考;而在工具选择上,他在项目“从 0 到 1”的冷启动阶段,极力推荐 Claude Code 等终端工具,以便更好地统览全局,一次性构建出合理的多文件项目架构。

或许你和我一样,对于 100% 基于 AI 的开发方式仍存疑惑。但在代码生成越来越廉价的今天,开发者的核心壁垒,正在加速向“需求精准拆解”、“系统架构把控”以及“面向报错的全局调度能力”转移。

工具

Slots:提高自定义 SwiftUI 组件设计效率的宏

将多个视图组合封装成可复用组件,是 SwiftUI 开发中的常见需求,对团队内部开发者或第三方库作者来说更是如此。但当组件包 title、icon、image、action 等多个泛型 View 插槽后,初始化器的组合数量往往会迅速膨胀。Kyle Bashour 创建的 Slots 宏,正是为了解决这类多 slot 组件的样板代码问题。

开发者只需声明组件的 slot 属性,宏便会按组合自动生成所需的初始化器,无需手写大量 init 重载。对于需要支持文本便捷写法的 slot,还可以通过 @Slot(.text) 自动获得 LocalizedStringKeyString 版本的初始化方式。 Slots 很适合用于构建设计系统中的 Card、Row、Banner、Toolbar 这类既要支持简单调用、又要保留高度定制能力的组件。


Explore SwiftUI:纯原生组件与修饰符的视觉速查图库

尽管 Apple 官方文档的质量在逐年改善,但对于以声明式和视觉驱动为主的 SwiftUI 来说,官方文档中依然缺乏足够直观的代码与 UI 效果对照,尤其是同一组件在 iOS、macOS 和 visionOS 等多平台上的表现差异。很多时候,开发者为了实现某个特定的 UI 细节,往往会去求助于复杂的第三方库或手写冗长的自定义视图,却忽略了 SwiftUI 本身可能已经提供了绝佳的原生解决方案。Florian 建立的 Explore SwiftUI 站点,正是一个为了解决这一痛点而生的“视觉速查字典”。它摒弃了任何第三方封装,纯粹以展示 Apple 官方内置组件的原生能力为核心。所有的代码示例都被剥离了无关的业务逻辑,保持极简,配以高质量的视觉预览,开发者只需“复制、粘贴、运行”即可直接验证效果。

书籍

SwiftUI Architecture: Patterns and Practices for Building Scalable Applications

这是一本 Mohammad Azam 在不久前出版的新书。它不是一本教你如何使用 VStack 或编写动画的入门书,而是一本纯粹探讨 SwiftUI 应用架构、数据流和现代工程化实践的进阶读物。

书中提供了大量直击生产环境痛点的解决方案,例如:如何构建全局的 Sheets 和 Toasts、如何利用 NavigationPath 设计解耦的多 Tab 编程式路由、以及如何使用 Property Wrapper 编写优雅的表单验证。尤为重要的是,作者并不是要向你灌输某种死板的架构模式,而是旨在帮助你建立真正的声明式心智模型。

或许有人觉得,在 AI 辅助编程盛行的时代,这类探讨架构的书籍还重要吗?借用 Mohammad Azam 在书中的观点:AI 让代码生成变得廉价,但也正因如此,系统架构的设计(边界的划分和状态所有权的明确)变得比以往任何时候都更加重要。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

如何在本地跑 Core ML 模型识别呼噜声,并用 iCloud 优雅同步?

作者 Flutter笔记
2026年4月2日 15:00

大家好,我最近开发了一款App《SleepDiary(睡眠声音日记)》。

9771791b1b272012179e60c5853cedc8.jpg

作为一款睡眠监测类 App,核心业务逻辑可以用一句话概括:

录一整夜的音,把打呼噜和说梦话的片段摘出来,最后生成睡眠报告。

看似简单,但在工程实现上却困难重重:

  1. 隐私与成本问题:长达 8 小时的音频绝对不能一整段传到服务器端,这不仅会直接把你的服务器带宽跑破产,还会被用户骂死(谁敢把在卧室一整夜的录音全传到网上?)。
  2. 性能与功耗问题:放在端侧跑模型,势必要使用长时间的后台保活,如何避免手机发热和 OOM (Out Of Memory)?

经过最近这段时间的研究,我用 AVFoundation + Core ML + SwiftData 的纯血原生技术栈把这套流程跑通了。今天就和大家分享一下我的实现思路与踩坑日记。

一、端侧的 AI:硬核从零训练自己的鼾声分类模型

最初的设计方案很简单粗暴:开个录音,每秒去判断分贝,超过阈值就保存。但这完全不行,深夜翻身的声音、空调声、外面的汽车声都会被误判。

市面上现成的声音分类模型要么太大(动辄上百MB),要么对“鼾声”、“梦话”这种特定场景不够敏锐。于是我决定硬核一点——自己动手,从收集数据开始训练一个专用的轻量级神经网络(SnoreWave.mlpackage)。

1.1 数据收集与模型训练

为了让模型足够精准,我花了大量时间收集开源数据集并结合自己实录的各种“打雷级”打呼声(最终 1.2w 条数据)。 把杂乱的音频转换成模型能“看懂”的输入是第一步——将音频流转化为梅尔频谱图(Mel-spectrogram) 。这相当于将一维的声音信号,变成了二维的图像图像特征,然后再喂给我用深度学习框架搭建的 CNN(卷积神经网络)进行分类。

模型训练收敛后,我依靠 coremltools 将其转换为了 Apple 原生支持的 .mlpackage。为了控制 App 包体积并保证低功耗运转,这个模型被我极致压缩,剥离了非必要分支,达到了极高的预测效率。

1.2 AVAudioEngine 实时截流送显

有了自己的模型,下一步就是在 iOS 端跑通流式推理。 我们不使用高层的 AVAudioRecorder,而是使用 AVAudioEngine。因为它允许我们通过 installTap 在音频流经过的过程中“截胡”到 AVAudioPCMBuffer

然后在端侧把这个 Buffer 原样转化成模型需要的数组输入:

// 截胡音频流的伪代码
let inputNode = audioEngine.inputNode
let format = inputNode.inputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 4096, format: format) { [weak self] (buffer, time) in
    // 捕获到音频帧后,交给我们自定义的分类器管线
    self?.audioCaptureService.processAudioBuffer(buffer)
}
audioEngine.prepare()
try audioEngine.start()

1.3 降维打击 OOM 崩溃:用 Actor 隔离模型生命周期

坑点来了!如果每次截取到一个 buffer,都在主线程或者随机的 Dispatch Queue 去实例化这个自定义模型进行预测,一晚上下来你的 App 必因为内存暴涨被系统强制 Kill 掉(Jetsam Event)。

解法:引入 Swift Actor 隔离与复用机制 

在《睡眠声音日记》中,我是用全局唯一的 Actor 来维持模型的单一生命周期,使用环形缓冲区去缓存几秒钟的声音片断,组合后一次性输出:

swift
actor EventDetectionPipeline {
    // 全局唯一持有我们自己训练好的模型实例
    private let model = try? SnoreWaveformCNN(configuration: MLModelConfiguration())
    
    func processAudioWindow(_ window: AudioWindow) async {
        // 将音频转化成梅尔频谱所需的 MLMultiArray
        guard let multiArray = window.toMLMultiArray() else { return }
        
        // 发起端侧离线推理
        if let prediction = try? model?.prediction(input: multiArray) {
            if prediction.classLabel == "snore" {
                // 命中目标:触发存储!
                await persistCapturedEvent(label: .snore)
            }
        }
    }
}

通过自己训练轻量级模型 + Actor 的串行数据处理,保证了模型资源的极致释放。即便后台连续疯狂推理 8 个小时,CPU 的平均占用率也能被压在极低的水平,用户即使整晚充着电,手机也完全不发烫。

二、存储的艺术:音频文件与 SwiftData 模型分离

识别完事件后,怎么持久化? 这引发了第二个大问题——千万别把音频这种大块二进制流全都写进 SwiftData 或者 Core Data!

2.1 相对路径是王道

我的存储策略是:结构化数据走 SwiftData(打点时间、标签量化数据),音频文件走沙盒原生写入。 在《睡眠声音日记》的 SleepEventRecord 模型中,我只存了一个相对路径(filePath)。

@Model
final class SleepEventRecord {
    var timestamp: Date
    var duration: TimeInterval
    var eventLabel: EventLabel // .snore, .speech, .cough
    var filePath: String? // 只存相对路径: "20240315/snore_0234.m4a"
    
    init(timestamp: Date, eventLabel: EventLabel) {
        self.timestamp = timestamp
        self.eventLabel = eventLabel
    }
}

为什么要相对路径?  因为沙盒路径(UUID)在每次应用重签或重新安装时是会变的。如果存绝对路径,第二天文件全找不到了!读取时,永远使用 FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(filePath) 动态拼装。

2.2 防治 iCloud 把服务器挤爆

录了一晚上的高音质 M4A 文件,如果不加限制,系统的 iCloud 备份会自动把它们全传上去。用户那可怜的 5GB iCloud 很快就会爆满。因此,我在写入音频文件后,立马用原生 API 给文件打上“拒绝备份”的 Tag:

var url = documentDirectoryURL.appendingPathComponent(fileName)
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true // 保护用户的 iCloud 空间!
try url.setResourceValues(resourceValues)

三、私有 CloudKit 的优雅同步体验

音频不用同步了,但我们的 SleepSessionRecord(当晚评分,鼾声次数统计,分析数据)需要跨设备(尤其是和 Apple Watch 联动时)和防止在用户删掉 App 重装后丢失。

以往做 Core Data + CloudKit 繁琐得让人想死。但在 iOS 17 的 SwiftData 下,它变成了真正的“优雅”:我们甚至可以动态控制它的开启闭合。

我把开关值存到了 NSUbiquitousKeyValueStore(KVS),根据这个从远程同步过来的用户偏好,动态初始化 ModelContainer

// 在 SleepDiaryApp.swift 入口处动态配置容器
var sharedModelContainer: ModelContainer = {
    let schema = Schema([SleepSessionRecord.self, SleepEventRecord.self])
    
    // 读取 UserDefaults/KVS 的 iCloud 开关
    let isCloudSyncEnabled = UserDefaults.standard.bool(forKey: "iCloudSyncEnabled")
    
    let configuration = ModelConfiguration(
        schema: schema,
        isStoredInMemoryOnly: false,
        // 如果开启,赋予 private 数据库标识;如果不开启,设为 .automatic 或本地优先
        cloudKitDatabase: isCloudSyncEnabled ? .private("iCloud.xxxxx") : .none
    )
    
    do {
        return try ModelContainer(for: schema, configurations: [configuration])
    } catch {
        fatalError("Could not create ModelContainer: (error)")
    }
}()

依靠云端大容量配额下的 .private 标识,只要用户打开 iCloud 同步,他们在换了新手机后重新下载 App,所有的睡眠数据历史记录就会像魔法一样哗哗哗回到列表中。


四、写在最后

开发《睡眠声音日记》的这段时间里,我最大的感触是:苹果的原生护城河真的很香。  只依靠一套 Swift 兵器库:从 SwiftUI 的丝滑动画绘制、到 HealthKit 获取深度睡眠的联动、再到 Core ML 的底层加速,这是以往杂糅其它中间件完全得不到的性能优势和开发爽感。

感兴趣的同行们,可以在 App Store 搜  “睡眠声音日记-SleepDiary”  下载把玩一下,有任何架构或技术点上的建议,大家评论区见,或者私下找我交流。也欢迎吐槽!

❌
❌