阅读视图

发现新文章,点击刷新页面。

邪修!让显示器支持AI、远程、手势三种控制方式

大家好,我是石小石~


解锁明基RD270Q的新玩法

前不久,明基发布了最新款式的编程系列显示器 RD270Q,很荣幸我获得了优先体验资格。刚开箱,我就被它出众的颜值所吸引。

这款显示器保留了RD系列最核心也是我最喜欢的「编程模式」,而且它还升级到144Hz 高刷 并增加了彩纸模式。这使得在长时间编码下,它能极大缓解眼部疲劳,体验感非常舒适。

接下来,我会分享借助RD270Q配套的DisplayPilot2软件,结合AI与编码,如何玩转显示器的特色功能:

  • 用 Claude code 切换显示器编程模式

  • 用手机远程操控显示器锁屏

  • 用手势实现显示屏亮度调节 (动图帧率问题,图片效果不是很明显)

同时,我会结合长时间的编码体验,验证它是否能成为程序员必备的专业显示器。

显示器控制的核心——Display Pilot 2

无论是通过 AI、手机远程还是手势来控制显示器,核心本质都是依靠电脑上运行的 “脚本” 去操控显示器硬件。借助一些键鼠模拟脚本(如 Node 的robotjs、nut-js,或Python的keyboard),我们可以通过模拟鼠标事件来间接操控软件实现功能,比如通过 Node.js 脚本实现自动移动鼠标,并双击启动软件的自动化操作:

对应代码如下:

const { mouse,straightTo,Point,Button} = require("@nut-tree-fork/nut-js");
(async () => {
  // 移动鼠标到指定位置
  await mouse.move(straightTo(new Point(10, 10)));
  console.log("鼠标移动完成!");
  // 点击鼠标
  await mouse.doubleClick(Button.LEFT);
  console.log("执行完成!");
})();

可以看出,一些复杂的软件操作,通过模拟鼠标实现还是非常麻烦的,最重要的是脚本几乎无法控制硬件。

幸运的是,明基 RD270Q 自带了配套软件 Display Pilot 2,它可以直接通过软件快速调用显示器的硬件级操作能力,以满足我们编程中的个性化控制需求。参考软件截图,它拥有非常多的显示屏操作功能,且基本都支持通过快捷键操作。

思路到这里就很清晰了:我们完全可以编写脚本,模拟键盘事件触发 Display Pilot 2 的快捷操作,从而间接实现对显示器的控制。

使用Clade code+skills控制显示屏

编程模式切换效果演示

编程模式是明基 RD 系列显示器的特色功能,在深色模式下,显示器会通过硬件级算法强化语法高亮效果,以提升长期编程的舒适度;RD270Q新增的彩纸模式,则能让界面产生类纸感的细腻色彩,满足深度护眼需求。如下图,在黑暗模式下,明基对代码的显示优化非常明显,代码对比更加鲜明,不刺眼。

而且它还搭载了莱茵认证的抗反射抗面板,即便在强光环境下使用,屏幕也不会刺眼、不产生明显眩光,长时间观看依旧舒适。

在配套软件的基础上,我们能否借助 AI 实现这些显示模式的一键自动切换呢?答案是完全可以。 比如,直接通过 AI 对话下达指令,让显示器自动切换至电子书模式

或是通过指令让 AI 精准调节屏幕亮度、音频大小等参数

原理分析——RD270Q-Opera-skills

Claude Code 为例,我们来实现这一效果。需要明确的是:AI 本身并不能直接操控显示器硬件,即便它能生成脚本,也不知道如何与显示器交互。因此,我们可以通过自定义技能(Skills) —— 比如创建一个 RD270Q-operation-skills,来为 AI 扩展控制显示器的能力。

如果你不了解 Skills,请自行百度。

该技能的项目结构如下:

RD270Q-operation-skills/
├── SKILL.md              # 元数据与指令定义
├── index.js              # 主入口:命令解析与分发
├── package.json          # 项目依赖配置
├── test.js               # 功能测试脚本
├── scripts/              # 底层操作模块
│   ├── keyboard.js       # 键盘快捷键封装
│   └── mouse.js          # 鼠标操作封装
└── references/           # 参考文档
    └── 快捷键表.md        # Display Pilot 2 完整快捷键

整个技能的核心逻辑非常简单: 将 Display Pilot 2 的快捷键功能在代码中做映射,让 AI 可以通过函数调用触发。

示例核心代码(scripts/keyboard.js):

// 键盘快捷键模块 - 封装 Display Pilot 2 所有控制功能
const { keyboard, Key } = require("@computer-use/nut-js");

// 执行快捷键组合
async function executeShortcut(...keys) {
  await keyboard.pressKey(...keys);
  await new Promise(resolve => setTimeout(resolve, 100));
  await keyboard.releaseKey(...keys);
}

// ==================== 色彩模式 ====================
// 循环切换色彩模式 Ctrl+Alt+C
async function cycleColorModes() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.C);
}
// 编程亮模式 Ctrl+Alt+1
async function setCodingLight() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.Num1);
}
// 编程暗模式 Ctrl+Alt+2
async function setCodingDark() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.Num2);
}
// 编程纸张模式 Ctrl+Alt+0
async function setCodingPaper() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.Num0);
}
// ..... 其他快捷操作


// 导出所有方法
module.exports = {
  executeShortcut,
  cycleColorModes,
  setCodingLight,
  setCodingDark,
  setMBook,
  // ...
};

我们只需要在 SKILL.md 中规范好 AI 的调用方式与指令规则,完成整套技能开发后,Claude Code 就拥有了直接操控显示器模式的能力,使用体验直接拉满。

除了编程模式的切换,凡是 Display Pilot 2 能通过快捷键实现的显示器操控功能,这个skills都能完美胜任,甚至像Display Pilot 2屏幕分区这样的高级功能,也能通过控制鼠标来模拟实现。

使用手机远程控制显示屏

很多时候,我们可能临时有事需要离开工位,如果我们突然想锁屏或者想远程控制一下鼠标执行某个简单操作就必须立刻回到工位才行。基于这中场景,实现手机远程控制显示器就非常有意义。

如下图,就是根据明基RD270Q支持的快捷键开发的一个移动端操作界面,并增加了鼠标触摸移动控制功能。

远程锁屏、鼠标控制演示

如果外出忘记锁屏,通过手机实现这个功能非常方便实用。

此外,通过移动端界面的触控区域,我们还能远程操控鼠标移动、直接打开 VSCode 等软件。是不是有点Todesk青春版的感觉?

除此之外,其他快捷操作,如编程模式、亮度调节、夜间保护调节等功能都是支持的,这里也就不一一展示了。

原理分析——websoket+node控制快捷键

远程控制的方案其实非常简单:核心就是跑在本地的一个 Node 脚本,用来模拟键盘、鼠标操作,间接通过 Display Pilot 2 控制显示器。同时启动一个 Web 服务提供移动端操作界面,借助 WebSocket 实现手机与 Node 服务实时通信,最终完成远程控制。简单涞水,就是Web 端通过WebSocket 控制本地端Node服务模拟系统快捷键操作

前端就是一个普通的 Vue 项目 , 页面上放几个控制按钮,点击时通过 WebSocket 向 Node 服务发送对应指令:

function createWebSocketServer(server) {
  const wss = new WebSocket.Server({ server, path: "/ws" });
  wss.on("connection", (ws) => {
    console.log("移动端已连接");
    ws.on("message", async (msg) => {
        const { type, action, params } = JSON.parse(msg);
        // 鼠标操作
        if (type === "mouse") {
          if (action === "move") {
            // 鼠标移动
            await mouse.move(params.x, params.y);
          } else if (action === "click") {
            // 鼠标点击
            await mouse.click(params.button);
          }
        }
        // 键盘操作
        if (type === "keyboard"){
          
        }
    });
  });
}

Node 端主要搭建 WebSocket 服务,接收移动端指令并执行系统操作。

const app = express();
const server = http.createServer(app);

// 初始化 WebSocket 服务
createWebSocketServer(server);

server.listen(PORT, () => {
  console.log(`WS 服务已启动:ws://localhost:${PORT}/ws`);
});;

具体的鼠标移动、键盘快捷键等逻辑,统一封装在 mouse.jskeyboard.js 中,底层依赖node第三方库nut-js实现鼠标和快捷键控制。

使用手势控制显示屏

RD270Q 还有个我觉得特别实用的功能 ——Visual Optimizer 视觉优化。它通过内置光传感器,能根据环境光智能同步调节屏幕亮度与色温,降低屏幕与环境的明暗反差,配合编码深色模式,长时间看代码也更柔和护眼。

不仅如此,我们还可以通过Display Pilot 2进一步调整屏幕亮度,实现个性化需求。基于Display Pilot 2,我们还能实现通过手势控制实现显示器的隔空操作,作为技术创意尝鲜、趣味交互玩具,还是得研究和尝试的。

桌面版的手势识别存在一定技术难度,恰好之前我有写过类似的技术文章:油猴+手势识别:我实现了任意网页隔空控制!索性偷个懒,在网页上实现手势识别用来控制显示器。先看看Demo效果:

  • 左手张开 + 右手滑动,即可调低屏幕亮度(左手握拳 + 右手滑动,即可调高屏幕亮度)

  • 右手握拳,可以实现一键锁屏功能

它的核心实现是基于MediaPipe,这是一个是谷歌开源的跨平台、实时轻量级多媒体机器学习框架,支持 Python、JS 等多种编程语言,借助它能轻松实现桌面级的手势识别功能。

如果你对相关技术感兴趣,可以看看这个实现

Demo:油猴+手势识别:我实现了任意网页隔空控制!

代码:《有趣的手势识别、人脸识别脚本》

Flow 智能工作流

本来我还在琢磨,能不能通过 AI 指令或远程控制,自己搭一套编码时的专属显示方案,比如打开 VS Code 就自动切换到我习惯的亮度、护眼参数等。结果发现 RD270Q 早已自带了 Flow 智能工作流,在 Display Pilot 2 里提前预设好编程、文档、设计等场景后,打开对应软件就能自动切换显示参数,省去反复调节的麻烦,真正实现了 “打开即用” 的智能个性化体验。

结语

从借助 AI 指令、移动端远程控制显示器,到创意十足的手势隔空控制,这篇文章我通过三种个性化玩法,把RD270Q显示器的自定义操控能力发挥到了极致。这些功能实现的核心,离不开Display Pilot 2对显示器本身的 稳定操控能力。

当然,即便不借助这款软件,文中的思路也可以延伸到电脑本身的快捷操作、系统级功能调用上,大家不妨顺着这个方向自行尝试拓展。

写完这篇文章已是凌晨,144Hz 高刷屏搭配显示器的深色编码模式,长时间使用眼部依然舒适,没有出现干涩、疲劳感。实际体验下来,RD270Q 的护眼技术确实做得不错,整体感受很好。

总而言之,新款 RD270Q 不仅保留了核心优势,价格也很有诚意,三千出头,上市期间会更优惠!兄弟们,不用犹豫,这次可以放心冲了。当然,要是追求极致编程体验 RD280URD280UGRD320U也也都是非常不错的选择。

最后, 附上一张深夜codding的图,希望这篇分享能为大家带来一些实用参考。

从网关的角度理解并实现一个 Mini OpenClaw

1. 前言

OpenClaw 与其他 AI Agent 最本质的区别是什么?首先,OpenClaw 本身也是一个 AI Agent,但关键在于它能连接多种 IM 渠道,并利用这些 IM 工具提供的开发能力来调用自身的 Agent——这种能力被称为“网关”。因此,有后端的技术大咖将 OpenClaw 总结为:OpenClaw = 高权限 AI Agent + 网关

所以只有理解了 OpenClaw 的本质之后,我们才可以实现一个 Mini OpenClaw。

首先我们要实现一个网关,那么网关是什么呢?

网关对于后端的同学来说,肯定不陌生。在 Spring Boot 微服务架构中,API 网关已成为标准的基础设施组件,其核心作用与 OpenClaw 中的“网关”如出一辙:对外隐藏后端的实现细节(服务地址、版本、熔断等),对内统一通信协议,并提供横切能力(如鉴权、限流、日志等) 。两者的区别仅在于作用对象不同——OpenClaw 的网关面向 IM 渠道(消息协议适配),而后端网关面向 HTTP/RPC 调用(协议转换与流量管理)。

所以 OpenClaw 的所谓网关就是一个消息协议适配器。

所以我们先要实现网关最核心的功能:协议适配。这是网关最本质的能力——对外讲 IM 的方言,对内统一说普通话。

2. 网关核心功能:协议适配

不同 IM(飞书、微信 等)的消息格式千差万别:有的用 user_id,有的用 from 字段,有的消息正文可能嵌套在 text 或 message 对象中。我们可以通过设计一个消息协议将这些差异全部“抹平”,这样本地 AI Agent 就只依赖这标准消息协议,无需关心消息来自哪个渠道。

设计一个入站的消息对象 InboundMessage:

# events.py
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class InboundMessage:
    """从聊天频道接收到的消息"""
    channel: str  # 用于区分来源,后续发送回复时需要知道应该调用哪个 IM 的 API(feishu、wechat)
    sender_id: str  # 用户标识符
    chat_id: str  # 聊天/频道标识符
    content: str  # 消息文本
    timestamp: datetime = field(default_factory=datetime.now)  # 消息时间

这样新增一个 IM 渠道时,只需要写一个适配器将私有消息转换成 InboundMessage 即可,其余代码零改动。

简而言之:设计 InboundMessage 就是为了让网关“对外讲方言,对内讲普通话”,所有渠道的消息到达网关后立刻被标准化,Agent 只需处理这一种标准格式。

同样地不同 IM 的发送接口千差万别:飞书需要 receive_id,微信需要 touser,Telegram 需要 chat_id。通过设计一个 OutboundMessage 消息对象,这样 Agent 只需要产出 channelchat_idcontent 三个核心字段,网关再根据 channel 值调用对应的 IM 适配器,由适配器负责转换成目标 IM 的私有请求格式即可。

OutboundMessage 消息对象的字段设计如下:

# events.py
@dataclass
class OutboundMessage:
    """要发送到聊天频道的消息"""
    
    channel: str
    chat_id: str
    content: str
    reply_to: str | None = None # 支持引用回复,用于指明当前回复的是哪一条历史消息

网关的输入是 InboundMessage,输出是 OutboundMessage,这样本地 AI Agent 核心只处理这两种标准格式信息,完全不依赖任何 IM 私有 API。这使得添加新 IM 渠道变得非常简单:只需要写一个适配器,将 InboundMessage 解析出来,并将 OutboundMessage 转换成该 IM 的发送请求即可。因为本地 AI Agent 完全不知道自己在和谁在交互,它只看到 InboundMessage/OutboundMessage,这正是网关隐藏后端实现细节的精髓,也是网关本质的体现

3. 网关内部路由:统一通信总线

根据前面的设计,我们已经将各个 IM 渠道的消息统一成了 InboundMessage,并将 Agent 的回复统一成了 OutboundMessage。但仅仅统一格式还不够,还需要解决一个核心问题:多个渠道的消息并发涌入,而 Agent 的处理可能是同步/半异步的,如何让它们有序、可靠、不互相阻塞?

这就需要一个统一通信总线——本质上是一个轻量级的内部消息路由。而最经典、最可靠的实现方式就是双队列解耦

入站异步队列: 渠道 → Agent
出站异步队列: Agent → 渠道

通过双队列把网关内部的“消息流动”标准化为两个 FIFO 管道:

  • 入站异步队列:所有 IM 渠道的消息汇聚点,Agent 从这头取“原材料”。
  • 出站异步队列:所有回复的汇聚点,分发器从这头取“成品”并发送。

为什么需要这样设计?

每个 IM 渠道(飞书、微信等)都有自己的 Webhook 或长连接,当瞬间收到大量消息(例如群聊刷屏)时,如果直接在回调中同步调用 Agent,Agent 处理耗时较长,会导致 Webhook 超时、连接堆积,甚至被 IM 服务器屏蔽。

我们让每个渠道适配器只做最轻量的事情,每当接收到消息时,就只需要解析消息、封装成上述设计的 InboundMessage,然后立即推送到入站异步队列中,马上返回返回即可。而 Agent 的处理则由一个独立的后台协程从入站异步队列中拉取,这样生产者和消费者的速度完全解耦。即使 Agent 处理得慢,队列也能起到“缓冲”作用,不会丢消息。

同时 Agent 只产出上述设计的 OutboundMessage 的数据并推送到出站异步队列中。另一个独立的分发器协程从出站异步队列中取出消息,找到对应的渠道适配器,调用该适配器的发送方法进行发送消息。这样一来,Agent 完全不需要知道消息要发往哪里、怎么发,路由逻辑全封装在网关内部。

统一通信总线代码实现如下:

# message_bus.py
"""用于解耦频道与智能体通信的异步消息队列"""
import asyncio
from loguru import logger
from events import InboundMessage, OutboundMessage

class MessageBus:
    """
    异步消息总线,用于将聊天频道与智能体核心解耦。
    频道将消息推送到入站队列,智能体处理它们并将响应推送到出站队列。
    """
    def __init__(self):
        # 入站异步队列
        self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()
        # 出站异步队列
        self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()
    
    async def publish_inbound(self, msg: InboundMessage) -> None:
        """将来自频道的消息发布给智能体"""
        await self.inbound.put(msg)
    
    async def consume_inbound(self) -> InboundMessage:
        """消费下一条入站消息(阻塞直到有消息可用)"""
        return await self.inbound.get()
    
    async def publish_outbound(self, msg: OutboundMessage) -> None:
        """将智能体的响应发布给频道"""
        await self.outbound.put(msg)
    
    async def consume_outbound(self) -> OutboundMessage:
        """消费下一条出站消息(阻塞直到有消息可用)"""
        return await self.outbound.get()

同时入站异步队列和出站异步队列通过 asyncio.Queue 提供。asyncio.Queue 是异步编程中实现生产者-消费者模式的标准工具,它让不同协程之间可以安全、非阻塞地交换数据。在我们上述网关的设计中,正是依赖它实现了入站/出站双队列解耦,从而让多个 IM 渠道可以并发接收消息,同时 Agent 通过并发处理消息,实现效率提高。没有它,你就得自己用锁和条件变量实现类似功能,既复杂又容易出错。

接着我们修改上一篇文章《如何使用飞书机器人连接本地 AI Agent》中实现的飞书连接本地 AI Agent 的飞书频道,实现将来自飞书的消息转发到通信总线。

# feishu.py
+ from events import InboundMessage
+ from message_bus import MessageBus

class FeishuChannel:
    """极简版飞书 WebSocket 长连接机器人"""
+    name = "feishu"
    def __init__(self, config: FeishuConfig, bus: MessageBus):
        self.config = config
        self.bus = bus
        # 省略...

    async def start(self) -> None:
        # 省略...
-    def _on_message(self, data: P2ImMessageReceiveV1) -> None:
+    async def _on_message(self, data: P2ImMessageReceiveV1) -> None:
        """接收到消息时的回调"""
        msg = data.event.message
+        sender = data.event.sender
        # 只处理用户发送的纯文本消息
        if data.event.sender.sender_type == "bot" or msg.message_type != "text":
            return

        content = json.loads(msg.content).get("text", "")
        if not content:
            return
        
+        # 提取发送者信息
+        sender_id = sender.sender_id.open_id if sender.sender_id else "unknown"
+        # 获取用于回复的 chat_id
+        chat_id = msg.chat_id
+        chat_type = msg.chat_type  # "p2p" 或 "group"
+        reply_to = chat_id if chat_type == "group" else sender_id
+        # 将消息转发到总线
+        await self._handle_message(
+            sender_id=sender_id,
+            chat_id=reply_to,
+            content=content,
+        )
-        # 启动独立线程处理 AI 逻辑并回复,防止阻塞 WebSocket 接收循环
-        # threading.Thread(
-        #     target=self._process_and_reply, 
-        #     args=(msg.chat_id, content)
-        # ).start()

+    async def _handle_message(
+        self,
+        sender_id: str,
+        chat_id: str,
+        content: str,
+    ) -> None:
+        """
+        处理来自聊天平台的传入消息。
+        此方法将消息转发到总线。
        
+        参数:
+            sender_id: 发送者的标识符。
+            chat_id: 聊天/通道的标识符。
+            content: 消息文本内容。
+        """
        
+        msg = InboundMessage(
+            channel=self.name,
+            sender_id=str(sender_id),
+            chat_id=str(chat_id),
+            content=content
+        )
        
+        await self.bus.publish_inbound(msg)

现在我们已经将飞书发过来的消息推送到通信总线中了,接着我们需要在 Agent 异步处理协程中循环读取总线中的消息进行处理了。

4. 实现并发 Agent Loop

我们上文讲到了通过 asyncio.Queue 实现了入站/出站双队列解耦,从而让多个 IM 渠道可以并发接收消息,同时 Agent 通过并发处理消息,实现效率提高。

但我们前面实现的 Agent Loop 的同步处理数据,所以我们需要重新设计并实现我们的 Agent Loop。

首先我们这个 Agent Loop 需要具备以下功能点:

  1. 持续运行:只要网关没有关闭,Agent Loop 就要一直工作,不能退出。
  2. 响应及时:当有新消息到达时,应尽快开始处理,避免不必要的延迟。
  3. 可优雅停止:外部可以调用 stop() 方法,让循环在安全时机退出,而不是强制杀死协程。
  4. 容错性:单条消息处理失败不应导致整个循环崩溃,并且要能告知用户出错。

那么第一个功能点持续运行,我们可以通过使用一个布尔标志控制循环是否继续。

self._running = True
while self._running:
    # 只要 self._running = True 就一直循环读取通讯总线中的消息进行处理

这样只要 self._running = True 就一直循环读取通讯总线中的消息进行处理。同时我们设计一个 stop() 方法设置 self._running = False,这样外部协程就可以调用 stop() 使得循环将在下一次条件判断时退出。

在读取通讯总线中的消息时,我们需要通过 asyncio.wait_for 实现可中断阻塞读取。即如下实现:

self._running = True
while self._running:
    # 只要 self._running = True 就一直循环读取通讯总线中的消息进行处理
    msg = await asyncio.wait_for(
        self.bus.consume_inbound(), # 本质是 await inbound_queue.get()
        timeout=1.0,
    )

如果不使用 asyncio.wait_for 而是直接使用 await self.bus.consume_inbound() 的话,没有消息就一直等着,那么循环永远不会走到 while self._running 的条件判断。此时调用 stop() 设置 self._running = False 是无效的,因为协程卡在 get() 上,永远没有机会检查 self._running 标志。

而使用 asyncio.wait_for 并设置超时为 1 秒,也就是如果 1 秒内返回了消息,就正常得到 msg。如果 1 秒后队列仍为空,wait_for 会抛出 asyncio.TimeoutError。这样,协程最多阻塞 1 秒就会醒来一次,重新检查 while self._running。因此,即使没有消息,循环也能每秒检查一次退出标志,实现可中断的阻塞读取

根据上述设计我们初步实现 Agent Loop 如下:

import asyncio
import json
import os
from typing import Any

from dotenv import load_dotenv
from loguru import logger
from openai import AsyncOpenAI

from events import InboundMessage, OutboundMessage
from message_bus import MessageBus

load_dotenv()

class AgentLoop:
    def __init__(
        self,
        bus: MessageBus,
        max_iterations: int = 200,
        api_key: str | None = None,
        base_url: str = "https://api.deepseek.com",
        model: str = "deepseek-chat",
    ):
        self.bus = bus
        # 最大工具调用轮次,防止死循环
        self.max_iterations = max_iterations
        self.model = model
        self._running = False
        # 初始化 OpenAI异步客户端 兼容客户端(如 DeepSeek)
        self.client = AsyncOpenAI(
            api_key=api_key or os.getenv("DEEPSEEK_API_KEY"),
            base_url=base_url,
        )

    # ------------------------------------------------------------------
    # 主循环:持续消费 入站异步队列
    # ------------------------------------------------------------------

    async def run(self) -> None:
        """运行智能体循环,处理来自总线的消息。"""
        self._running = True
        logger.info("Agent loop started")

        while self._running:
            try:
                # 从入站队列消费下一条消息,设置超时以便能定期检查 _running 标志
                msg = await asyncio.wait_for(
                    self.bus.consume_inbound(),
                    timeout=1.0,
                )
                try:
                    # 处理消息并获取响应
                    response = await self._process_message(msg)
                    if response:
                        # 将响应发布到出站队列
                        await self.bus.publish_outbound(response)
                except Exception as e:
                    logger.error(f"Error processing message: {e}")
                    await self.bus.publish_outbound(
                        OutboundMessage(
                            channel=msg.channel,
                            chat_id=msg.chat_id,
                            content=f"抱歉,处理消息时出错:{e}",
                        )
                    )
            except asyncio.TimeoutError:
                continue

    def stop(self) -> None:
        """停止智能体循环。"""
        self._running = False
        logger.info("Agent loop stopping")

上述的 run 方法需要在一开始就启动,这样才可以实现一有消息就马上处理,而不会漏消息。我们把上一篇讲解实现飞书接入本地 AI Agent 的启动文件 test_feishu.py 重命名为 gateway.py,也就是网关的意思,并且修改其中的启动代码:

+ from message_bus import MessageBus
+ from loop import AgentLoop
async def main():
    # 1. 填入你的飞书机器人凭证
    config = FeishuConfig(
        app_id="xxx",         # 替换为真实的 App ID
        app_secret="xxx",    # 替换为真实的 App Secret
        encrypt_key="",                      # 如果飞书后台配置了 Encrypt Key 则填入,否则留空
        verification_token=""                # 如果配置了 Verification Token 则填入,否则留空
    )
+    deepseek_key = os.getenv("DEEPSEEK_API_KEY", "")
+    bus = MessageBus()
+    agent = AgentLoop(
+        bus=bus,
+        api_key=deepseek_key,
+        base_url="https://api.deepseek.com",
+        model="deepseek-chat",
+        max_iterations=20,
+    )
    
    # 2. 初始化频道并启动长连接
-    channel = FeishuChannel(config=config)
+channel = FeishuChannel(config=config, bus=bus)
    
    logger.info("正在启动飞书机器人长连接...")
    
-    # 3. 启动并保持运行
+    # 3. 并发运行
    try:
-        await channel.start()
+        await asyncio.gather(
+            agent.run(),          # 持续消费 inbound 队列,调用 LLM
+            channel.start(),      # 飞书启动
+        )
    except KeyboardInterrupt:
        logger.info("收到退出信号,正在关闭...")

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

通过上述修改我们就实现了 Agent 和飞书频道在初始化的时候并发运行,从而实现了一开始就监听入站异步队列的消息。

上述 Agent Loop 的 self._process_message 方法是还没实现的,所以我们继续实现 Agent 对消息的处理。本质就是实现大模型的工具调用循环。

在实现 Agent 对消息的处理之前,我们先要重新设计一下会话历史。

5. 会话历史设计

在前面的文章中我们的会话历史就是一个数组,结构如下:

history = [
    {"role": "system", "content": getattr(agent, "SYSTEM", "你是一个助手")},
    {"role": "user", "content": content}
]

后续如果继续有消息就根据角色往数组 history 中追加用户消息和助手消息即可。

但在 OpenClaw 中需要保证不同渠道、不同群、不同用户的历史会话完全隔离。我们可以使用 dict[str, list[dict]] 作为存储结构,相当于在 JavaScript 中设置一个对象,然后通过 key 作为唯一标识进行会话隔离。

key 设计:

这个 key 我们可以设置由 channel + chat_id 组合而成,例如 "feishu:oc_xxx"。然后我们在之前设计的 InboundMessage 对象中设置一个 session_key 方法用于返回会话唯一标识。设置如下:

@dataclass
class InboundMessage:
    # 省略...
    
+    @property
+    def session_key(self) -> str:
+        """用于会话标识的唯一键"""
+        return f"{self.channel}:{self.chat_id}"

value 设计:

value 其实就是上述的历史会话数组,即:

[
    {"role": "system", "content": getattr(agent, "SYSTEM", "你是一个助手")},
    {"role": "user", "content": content}
]

同时我们设计一个 _get_history 的函数来实现对会话历史的懒加载,如果 session_key 不存在,自动创建新列表并插入 system prompt,如果 session_key 存在则返回内部列表的直接引用,调用方可以修改它,即追加消息。这样设计可以避免拷贝带来的性能开销。

实现如下:

# ---------- 会话历史管理(按 session_key 隔离) ----------
# 全局字典:存储所有会话的对话历史
# - Key: session_key,用于唯一标识一个会话(例如 "feishu:chat_id")
# - Value: 消息列表,每个元素是 OpenAI API 兼容的消息字典(包含 role, content 等字段)
_sessions: dict[str, list[dict]] = {}

# 系统提示词:定义 AI 助手的角色、能力和行为准则
SYSTEM_PROMPT = (
    "你是一个智能助手,可以通过工具帮助用户完成任务。"
    "请简洁、准确地回答用户问题。"
)
# 获取会话历史
def _get_history(session_key: str) -> list[dict]:
    # 若为新会话,自动初始化一条包含 system prompt 的消息
    if session_key not in _sessions:
        _sessions[session_key] = [{"role": "system", "content": SYSTEM_PROMPT}]
    # 返回该会话的历史列表(引用,允许外部修改)
    return _sessions[session_key]

6. Agent Loop 的核心:消息处理

在完成了会话历史管理和主循环的可中断阻塞读取之后,Agent Loop 最核心的部分就是 单条消息的处理逻辑——即 _process_message 方法。该方法实现了 ReAct(推理+行动)模式:调用 LLM → 若需要工具则执行工具 → 将结果返回 LLM → 重复直到得到最终答案。下面详细解析其实现:

class AgentLoop:
    # 省略...

    # ------------------------------------------------------------------
    # 单条消息处理:tool-call 循环
    # ------------------------------------------------------------------
    async def _process_message(self, msg: InboundMessage) -> OutboundMessage | None:
        # 1. 获取当前会话的历史,并追加用户消息
        messages = _get_history(msg.session_key)
        messages.append({"role": "user", "content": msg.content})

        final_content: str | None = None
        # 2. 进入工具调用循环(最多 max_iterations 次)
        for iteration in range(self.max_iterations):
            # 3. 调用 LLM(异步非阻塞)
            response = await self.client.chat.completions.create(
                model=self.model,
                messages=messages, 
                tools=TOOLS,
                tool_choice="auto",
            )
            assistant_msg = response.choices[0].message

            # 将助手消息追加到历史
            messages.append(assistant_msg)

            # 4. 如果没有 tool_calls,说明任务完成
            if not assistant_msg.tool_calls:
                final_content = assistant_msg.content or ""
                break

            # 5. 执行所有工具调用,并将结果以 role=tool 追加到历史记录
            for tool_call in assistant_msg.tool_calls:
                name = tool_call.function.name
                args = json.loads(tool_call.function.arguments)
                logger.debug(f"Executing tool: {name}, args: {args}")

                result = _execute_tool(name, args)
                logger.debug(f"Tool result: {result[:100]}")

                messages.append(
                    {
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "name": name,
                        "content": result,
                    }
                )
        else:
            # 达到最大迭代次数
            final_content = "已达到最大处理轮次,无法给出最终答案。"

        if final_content is None:
            final_content = "处理完成,但没有内容返回。"
        # 6. 构造出站消息返回给用户
        return OutboundMessage(
            channel=msg.channel,
            chat_id=msg.chat_id,
            content=final_content,
        )

上述代码的实现跟我们前面文章实现 Agent Loop 是一样的,所以大家还有不懂的话,可以回看前面文章的详细解析。最最重要的就是最后返回了构造了 OutboundMessage 格式的出站消息,然后在 run 方法中通过 self.bus.publish_outbound(response) 将消息发布到出站队列。

其中工具定义实现如下:

# ---------- 内置工具定义 ----------
TOOLS: list[dict] = [
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "读取本地文本文件内容。",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "文件路径"},
                    "encoding": {
                        "type": "string",
                        "enum": ["utf-8", "gbk"],
                        "description": "文件编码,默认 utf-8",
                    },
                },
                "required": ["path"],
            },
        },
    }
]

def _execute_tool(name: str, arguments: dict) -> str:
    """同步执行内置工具,返回字符串结果。"""
    if name == "read_file":
        from pathlib import Path

        path = arguments.get("path", "")
        encoding = arguments.get("encoding", "utf-8")
        try:
            p = Path(path).expanduser()
            if not p.exists():
                return f"❌ 文件不存在: {path}"
            return p.read_text(encoding=encoding)
        except Exception as e:
            return f"❌ 读取失败: {e}"
    return f"❌ 未知工具: {name}"

我们这里先只实现一个读取文件内容的工具,后续再实现更多的工具。

7. 构建网关的渠道层

7.1 为什么需要渠道层?

在上一小节中,我们实现在 Agent 中构造了 OutboundMessage 格式的出站消息,然后将消息发布到出站队列中。但还缺少关键的一环:出站异步队列中的消息由谁来消费?如何将 Agent 的回复正确地发送回原来的聊天频道?

我们知道每个即时通讯平台都有自己独特的 API 协议,如果让 Agent 直接处理这些差异,会导致 Agent 逻辑中混杂大量渠道特定代码,每增加一个渠道就要修改 Agent 核心逻辑,这会造成维护噩耗。

所以我们需要构建一个 渠道管理器(ChannelManager),作为网关的出站交通枢纽,负责管理所有 IM 适配器的生命周期,并将出站消息路由到正确的渠道。具体需要实现以下功能:

  1. 注册与管理渠道实例

    • 运行时动态注册各个渠道
    • 维护渠道状态信息
    • 提供统一的渠道访问接口
  2. 协调启动与停止流程

    • 控制渠道启动顺序,避免竞态条件
    • 实现优雅停止,防止消息丢失
    • 处理异常情况下的资源清理
  3. 消息路由与派发

    • 根据消息的 channel 字段路由到正确渠道
    • 调用渠道的发送方法
    • 实现错误隔离和重试机制

7.2 渠道层的设计与实现

如果把整个网关系统比作一个繁忙的交通枢纽,那么渠道层就是站在十字路口中央的交警。它不亲自运送货物,但指挥着所有运输车辆有序通行。

具体来说,渠道层连接着:

  • 上游:内部消息总线(MessageBus),接收标准化的出站消息
  • 下游:各个 IM 渠道适配器(FeishuChannel、WechatChannel 等)

我们先实现一个 ChannelManager 类,并实现数据结构与初始化。代码如下:

import asyncio
from loguru import logger
from message_bus import MessageBus
from feishu import FeishuChannel


class ChannelManager:
    def __init__(self, bus: MessageBus):
        self.bus = bus
        # 存储已注册的渠道适配器,key 为渠道名称(如 "feishu")
        self.channels: dict[str, FeishuChannel] = {}
        # 出站分发器的任务句柄,用于优雅停止
        self._dispatch_task: asyncio.Task | None = None

ChannelManager 的核心数据结构 channels 是一个字典: channel_name → 适配器实例

  • Key = 渠道名称(如 "feishu"、"wechat")
  • Value = 渠道实例对象

这个设计实现了运行时动态注册,可以在不重启服务的情况下添加新渠道。

接着我们来实现注册渠道功能:

class ChannelManager:
    def __init__(self, bus: MessageBus):
        # 省略...
    def register(self, channel: FeishuChannel) -> None:
        """注册一个渠道适配器。要求该适配器必须有 name 属性和 send 方法。"""
        self.channels[channel.name] = channel
        logger.info(f"Channel registered: {channel.name}")

上述注册渠道的代码实现看起很简单,其实背后的设计原理一点也不简单。它应用了工厂模式 + 依赖注入的设计模式。

  1. 工厂模式体现在:渠道的创建由外部完成,ChannelManager 只负责使用
  2. 依赖注入体现在:渠道实例通过 register() 方法注入,而非在 ChannelManager 内部创建

我们已经实现了一个飞书渠道 FeishuChannel,所以现在需要通过以下方式进行注册飞书渠道:

manager.register(FeishuChannel(...))

同时将来如果我们想新增一个微信渠道,就可以这样实现了,先实现一个 WechatChannel,然后:

manager.register(WechatChannel(...))

这样网关核心代码零改动,真正实现了"开闭原则":对扩展开放,对修改关闭。

接着实现启动所有已注册的频道以及出站分发器。

代码实现如下:

class ChannelManager:
    def __init__(self, bus: MessageBus):
        # 省略...
    def register(self, channel: FeishuChannel) -> None:
        # 省略...
    async def start_all(self) -> None:
        """启动所有已注册的频道以及出站分发器。"""
        if not self.channels:
            logger.warning("No channels registered")
            return

        # 先启动出站分发器协程(确保一有出站消息就能被处理)
        self._dispatch_task = asyncio.create_task(self._dispatch_outbound())

        # 并发启动所有渠道(每个渠道的 start 方法负责建立长连接或监听 Webhook)
        tasks = []
        for name, channel in self.channels.items():
            logger.info(f"Starting {name} channel...")
            tasks.append(asyncio.create_task(channel.start()))

        # 注意:通常渠道的 start 会永久阻塞(如 WebSocket 循环),因此 gather 不会返回
        await asyncio.gather(*tasks, return_exceptions=True)

我们上述的代码实现了一个看似简单却至关重要的设计决策,就是先启动分发器再启动渠道。那么为什么先启动分发器再启动渠道呢?

主要是为了防止消息丢失与响应延迟。让我们分析两种启动顺序的后果:

场景 A:先启动渠道,后启动分发器 时间线:

  1. 飞书渠道启动成功 ✓
  2. 用户立即发送消息:"你好"
  3. Agent 快速处理,生成回复:"你好!我是AI助手"
  4. 回复进入出站队列...
  5. 但是!分发器还没启动 ❌
  6. 回复消息在队列中堆积
  7. 用户等待...等待...(用户体验差)

场景 B:先启动分发器,后启动渠道(我们采用的方式) 时间线:

  1. 分发器启动,开始监听出站队列 ✓
  2. 飞书渠道启动成功 ✓
  3. 用户发送消息:"你好"
  4. Agent 处理,生成回复:"你好!我是AI助手"
  5. 回复进入出站队列
  6. 分发器立即发现新消息 ✓
  7. 路由到飞书渠道,立即发送 ✓
  8. 用户秒级收到回复(体验流畅)

在实际的生产环境经验中,"空转等待"比"忙中丢消息"要好得多。分发器提前就位,就像快递员提前在仓库门口等待,包裹一出来就能立即配送。

接着我们实现出站消息分发器

代码实现如下:

class ChannelManager:
    def __init__(self, bus: MessageBus):
        # 省略...
    def register(self, channel: FeishuChannel) -> None:
        # 省略...
    async def start_all(self) -> None:
        # 省略...
    async def _dispatch_outbound(self) -> None:
        """
        出站分发器:持续消费 outbound 队列,将消息发送到对应的渠道。
        这是一个后台协程,在 start_all 时启动。
        """
        logger.info("Outbound dispatcher started")

        while True:
            try:
                # 可中断阻塞读取,每隔1秒检查一次取消信号
                msg = await asyncio.wait_for(
                    self.bus.consume_outbound(),
                    timeout=1.0,
                )
                # 根据消息中的 channel 字段找到对应的适配器
                channel = self.channels.get(msg.channel)
                if channel:
                    try:
                        # 调用适配器的 send 方法(各渠道自己实现转换和发送逻辑)
                        await channel.send(msg)
                    except Exception as e:
                        logger.error(f"Error sending to {msg.channel}: {e}")
                else:
                    logger.warning(f"Unknown channel: {msg.channel}")

            except asyncio.TimeoutError:
                # 超时不是错误,只是没有消息,继续循环
                continue
            except asyncio.CancelledError:
                break

我们上一小节中所说的先启动分发器,本质就是通过 while True 不断循环使用 asyncio.wait_for 消费 outbound 队列,然后根据 msg.channel 路由并调用 send 方法。

设计亮点:

  1. 拉模式(Pull)而非推模式(Push)

    • 主动从消息队列拉取消息,控制权在自己手中
    • 相比回调式的推模式,更容易控制消费速率和错误处理
  2. 可中断的事件循环

    • timeout=1.0 让循环能定期"抬头看路",检查是否有停止信号
    • 没有这个超时,任务会一直阻塞在 consume_outbound() 上,难以优雅停止

接着我们继续实现渠道的发送方法,这是协议翻译的最后一步。

为了让 ChannelManager 能够统一管理,每个 IM 适配器必须实现以下两个成员:

  1. name: str:渠道唯一标识(如 "feishu")。
  2. async send(msg: OutboundMessage) -> None:发送回复的方法。

以飞书适配器为例,我们之前已经定义了 name = "feishu",现在补充 send 方法的实现:

class FeishuChannel:
    # 省略...
    async def send(self, msg: OutboundMessage) -> None:
        """通过飞书发送消息。"""
        if not self._client:
            logger.warning("飞书客户端未初始化")
            return

        try:
            # 根据 chat_id 格式确定 receive_id_type
            # open_id 以 "ou_" 开头,chat_id 以 "oc_" 开头
            if msg.chat_id.startswith("oc_"):
                receive_id_type = "chat_id"
            else:
                receive_id_type = "open_id"

            # 构建文本消息内容
            content = json.dumps({"text": msg.content})

            request = CreateMessageRequest.builder() \
                .receive_id_type(receive_id_type) \
                .request_body(
                    CreateMessageRequestBody.builder()
                    .receive_id(msg.chat_id)
                    .msg_type("text")
                    .content(content)
                    .build()
                ).build()

            # OpenAPI 调用是同步的,在线程中运行以避免阻塞
            response = await asyncio.to_thread(
                self._client.im.v1.message.create, request
            )

            if not response.success():
                logger.error(
                    f"发送飞书消息失败:code={response.code}, "
                    f"msg={response.msg}, log_id={response.get_log_id()}"
                )
            else:
                logger.debug(f"飞书消息已发送至 {msg.chat_id}")

        except Exception as e:
            logger.error(f"发送飞书消息时出错:{e}")

本质是就是将我们上一篇文章中的 FeishuChannel 类中 _process_and_reply 方法改成 send 方法即可。这样,ChannelManager 就可以统一调用 await channel.send(msg),完全不需要关心飞书 API 的具体细节。

8. 集成到网关启动入口

现在,我们将 MessageBus、AgentLoop、FeishuChannel 和 ChannelManager 全部串联起来。实现如下:

# gateway.py
import os
from loguru import logger
from feishu import FeishuChannel, FeishuConfig
from message_bus import MessageBus
from loop import AgentLoop
from manager import ChannelManager

async def main():
    # 1. 填入你的飞书机器人凭证
    config = FeishuConfig(
        app_id="xxx",         # 替换为真实的 App ID
        app_secret="xxx",    # 替换为真实的 App Secret
        encrypt_key="",                      # 如果飞书后台配置了 Encrypt Key 则填入,否则留空
        verification_token=""                # 如果配置了 Verification Token 则填入,否则留空
    )
    deepseek_key = os.getenv("DEEPSEEK_API_KEY", "")
    # 2. 创建总线
    bus = MessageBus()
    # 3. 创建 Agent 循环
    agent = AgentLoop(
        bus=bus,
        api_key=deepseek_key,
        base_url="https://api.deepseek.com",
        model="deepseek-chat",
        max_iterations=20,
    )
    
    # 4. 创建飞书渠道(传入总线,以便它 publish_inbound)
    feishu_channel = FeishuChannel(config=config, bus=bus)
    # 5. 创建渠道管理器,并注册飞书渠道
    channels = ChannelManager(bus=bus)
    channels.register(feishu_channel)
    
    logger.info("正在启动 Mini OpenClaw 网关...")
    
    # 6. 并发运行
    try:
        await asyncio.gather(
            agent.run(),          # 持续消费 inbound 队列,调用 LLM
            channels.start_all(), # 飞书长连接 + 出向派发器
        )
    except KeyboardInterrupt:
        pass
    finally:
        logger.info("收到退出信号,正在关闭...")
        agent.stop()
        await channels.stop_all()

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

至此整个网关的运行流程如下:

1. 网关“通电”

  • 我们启动 manager.start_all(),它立刻做了两件事:
    • 先派一个“快递员”(_dispatch_outbound 后台任务)守在 发件箱(outbound 队列) 旁边,随时准备把回复送出去。
    • 然后接通 飞书这个“电话线”feishu_channel.start()),开始等待用户发消息。

2. 用户发来消息

  • 用户在飞书群里说了一句“帮我读一下 /tmp/note.txt”。
  • 飞书适配器收到这条“方言消息”,立即翻译成网关内部的 普通话(InboundMessage),然后丢进 收件箱(inbound 队列)

3. Agent 大脑开始思考

  • agent.run() 一直在盯着 收件箱,一看到有新消息就取出来。
  • 它调用大模型并可能执行工具(比如读取文件),最终生成一段回复文本。
  • 然后把回复包装成 标准包裹(OutboundMessage),扔进 发件箱(outbound 队列)

4. 快递员送货

  • 守在 发件箱 旁边的快递员(_dispatch_outbound)发现新包裹,看看上面写的“收件渠道”是 feishu
  • 他马上找到飞书适配器,把包裹交给它:“请发到这个 chat_id 的群里”。
  • 飞书适配器又把回复从 普通话 翻译回 飞书的方言,调用飞书 API 发回群里。

5. 用户看到回复

  • 用户收到助手返回的文件内容,整个流程结束。

我们上述的 channels.start_all() 方法是还没实现的,我们实现一下:

class ChannelManager:
    def __init__(self, bus: MessageBus):
        # 省略...
    async def start_all(self) -> None:
        # 省略...
    async def stop_all(self) -> None:
        """优雅停止所有渠道和出站分发器。"""
        logger.info("Stopping all channels...")

        # 第一阶段:取消出站分发器任务
        if self._dispatch_task:
            self._dispatch_task.cancel()
            try:
                await self._dispatch_task
            except asyncio.CancelledError:
                pass

        # 第二阶段:逐个停止渠道(每个渠道的 stop 方法应关闭连接、释放资源)
        for name, channel in self.channels.items():
            try:
                await channel.stop()
                logger.info(f"Stopped {name} channel")
            except Exception as e:
                logger.error(f"Error stopping {name}: {e}")

实现也很简单,首先停止出站分发器的任务,再逐个停止渠道的连接,释放资源。

接着我们启动网关:

python gateway.py

启动结果如下:

01.png

然后我们接着在上一篇文章中设置了的飞书机器人中进行发消息。

然后我们发现报错了:

image.png

报错原因是因为飞书 SDK 的 register_p2_im_message_receive_v1 要求注册一个同步回调函数(不能是 async def),但消息处理逻辑(如解析内容、发布到 MessageBus)是异步的。因此,我们需要实现一个跨线程调度适配器,用于将飞书 WebSocket 线程中的同步回调安全地桥接到 asyncio 主事件循环。

9. 跨线程调度适配器

首先我们需要保存主事件循环对象,我们是在网关启动文件 gateway.py 中通过 asyncio.run(main()) 启动的主循环。因为飞书 WebSocket 客户端运行在一个独立的后台线程中(见 threading.Thread(target=run_ws, daemon=True).start()),它的回调需要一个同步函数,但真正的消息处理逻辑 _on_message 是一个异步协程,需要被提交到主事件循环中执行,因为 MessageBus 等组件是绑定到主循环的。为了从另一个线程安全地将协程投递到主事件循环,就需要持有主事件循环的引用

先保存主事件循环对象:

class FeishuChannel:
    def __init__(self, config: FeishuConfig, bus: MessageBus):
        self.config = config
        self.bus = bus
+        self._loop = None
        self._client = lark.Client.builder() \
            .app_id(config.app_id) \
            .app_secret(config.app_secret) \
            .build()

    async def start(self) -> None:
        # 省略...
+        # 保存主事件循环对象
+        self._loop = asyncio.get_running_loop()
        def run_ws():
            # 省略...

接着我们创建了一个同步函数 _on_message_sync 作为 register_p2_im_message_receive_v1 的实际回调,然后在 _on_message_sync 中将真正异步的处理函数 _on_message 调度到主事件循环中执行。实现如下:

def _on_message_sync(self, data: "P2ImMessageReceiveV1") -> None:
    try:
        if self._loop and self._loop.is_running():
            # 将异步处理函数调度到主事件循环
            asyncio.run_coroutine_threadsafe(
                self._on_message(data),
                self._loop
            )
        else:
            # 备用方案:在新事件循环中运行
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            try:
                loop.run_until_complete(self._on_message(data))
            finally:
                loop.close()
    except Exception as e: logger.error(f"处理飞书消息时出错:{e}")

接着我们修改 register_p2_im_message_receive_v1 的实际回调函数为上述我们实现的 _on_message_sync

class FeishuChannel:
    def __init__(self, config: FeishuConfig, bus: MessageBus):
        # 省略...
    async def start(self) -> None:
        # 省略...
        # 注册接收消息事件处理函数 im.message.receive_v1
-        handler = builder.register_p2_im_message_receive_v1(self._on_message).build()
+        handler = builder.register_p2_im_message_receive_v1(self._on_message_sync).build()
        # 保存主事件循环对象
        self._loop = asyncio.get_running_loop()

总的来说就是在主事件循环中“记住”主循环对象,供后续其他线程通过 asyncio.run_coroutine_threadsafe 将协程调度回主循环执行,是实现跨线程异步任务调度

同时当主事件循环不存在时创建一个全新的临时事件循环,在当前线程(WebSocket 线程)中同步运行 self._on_message(data),执行完毕后关闭循环。

经过上述迭代后,我们再次启动我们的程序:python gateway.py

然我们再在飞书设置的 AI 机器人上跟我们的 Mini OpenClaw 进行对话,结果如下:

1cbfafacd6d84ef03bd64151f081c17a.jpg

然后我们再根目录下创建一个 test.txt 文件,内容为:“从网关的角度理解并实现一个 Mini OpenClaw”,然后在飞书设置的 AI 机器人输入:“帮我读取 test.txt 文件”,结果如下:

e85ee7fd4d5df8c7fa605994b44a19e4.jpg

至此我们的 Mini OpenClaw 就实现了。

10. 总结

经过上述文章我们可以更加透彻地理解为什么说 OpenClaw 可以简单总结为“高级 Agent + 网关”了。它把飞书、微信这些聊天软件的“方言消息”统一通过一个网关转成内部能听懂的“普通话”(InboundMessage),Agent 只处理这种标准消息。

为了防止消息太多堵死系统,用了两个队列(入站异步队列出站异步队列,相当于收信箱和发件箱)把接收和回复解耦开,像流水线一样互不干扰。Agent 处理完后把回复扔进发件箱,再由分发器根据渠道标签(feishu、wechat)转回对应平台的格式发回去。

这样一来,添加新平台就像加个翻译插件,核心代码完全不用动。最后用跨线程调度解决了飞书回调异步的问题。整个网关跑起来就是:用户发消息 → 标准化 → 入站队列 → Agent 思考(可调用工具)→ 出站队列 → 翻译回原平台 → 用户收到回复

上述实现也是港大开源的 Nanobot 的核心实现,Nanobot 可以说是 Python 版的 OpenClaw,是学习研究场景的轻量选择。

我是程序员Cobyte,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。

大人工智能时代下前端界面全新开发模式的思考(二)

第二章:工具的盛宴——主流AI前端开发生态深度解析

当变革来临时,最直观的体现就是工具的爆发。在AI前端开发领域,我们看到了一场真正的"工具的盛宴":从IDE插件到全栈生成平台,从设计转代码到运行时AI能力,各种工具百花齐放,各显神通。

据统计,2024年GitHub上新增的AI编程相关项目超过10万个,Star数总计超过1000万。这是一个前所未有的繁荣时代,也是一个令人困惑的时代——工具太多,选择变得困难。

本章将深入解析主流AI前端工具的架构原理、使用场景和技术差异,帮助你在这个纷繁复杂的生态中找到最适合自己的工具组合。


2.1 工具分层与定位矩阵

为了理清这个复杂的生态,我们可以将当前主流工具分为四个层次。这种分层不是人为的划分,而是基于工具的抽象层次和能力边界自然形成的。

2.1.1 四层工具架构

层级 代表工具 核心能力 技术架构 适用场景 学习曲线
IDE集成层 Cursor、Windsurf、GitHub Copilot 实时代码补全、重构、解释、多文件编辑 IDE插件 + LLM API + AST解析 日常开发主力、代码审查、重构
设计转代码层 v0.dev、Screenshot-to-Code、Galileo AI 设计稿→代码、截图→代码、文本描述→UI 视觉识别模型 + LLM生成 + 设计系统 快速原型、设计还原、探索性开发
全栈生成层 Bolt.new、Lovable、Replit Agent 自然语言→完整应用、零配置开发环境 WebContainer + AI Agent + 运行时 MVP验证、学习实验、全栈原型
运行时层 Vercel AI SDK、LangChain、LlamaIndex Streaming UI、Tool Calling、Agent编排 Provider抽象层 + 消息协议 + 流式传输 生产级AI应用、对话式界面、Agent系统

这四个层次并非互斥,而是互补。一个完整的前端AI开发工作流,往往需要同时使用多个层次的工具。

工具组合示例

实际项目工作流:

需求分析阶段:
  ├─ 使用ChatGPT/Claude进行需求梳理和架构讨论
  └─ 使用Whimsical/Miro进行概念设计

设计阶段:
  ├─ 使用v0.dev快速生成UI原型
  ├─ 使用Figma进行精细设计
  └─ 使用Screenshot-to-Code还原设计稿

开发阶段:
  ├─ 使用Cursor进行日常编码
  ├─ 使用GitHub Copilot加速样板代码编写
  ├─ 使用团队Prompt库标准化代码生成
  └─ 使用Vercel AI SDK集成AI功能

验证阶段:
  ├─ 使用Bolt.new快速验证完整流程
  └─ 使用Storybook测试组件

部署阶段:
  ├─ 使用Vercel/Netlify自动部署
  └─ 使用AI监控工具检测异常

2.1.2 选择工具的决策框架

面对众多工具,如何做出选择?建议使用以下决策框架:

Step 1: 明确需求场景

  • 是日常开发还是原型验证?
  • 是个人使用还是团队协作?
  • 是前端开发还是全栈开发?
  • 需要集成到现有项目还是从零开始?

Step 2: 评估工具维度

维度 权重 评估标准
功能匹配度 30% 是否满足核心需求?
学习成本 20% 上手难度如何?
生态成熟度 20% 社区活跃度、文档质量
成本效益 15% 免费/付费?性价比如何?
可迁移性 15% 是否容易迁移到其他工具?

Step 3: 小规模试验

  • 不要一次性全面采用新工具
  • 选择一个小项目或功能模块试用
  • 收集团队反馈,评估实际效果

Step 4: 渐进式推广

  • 从愿意尝试的早期采用者开始
  • 建立使用规范和最佳实践
  • 逐步扩大到整个团队

2.2 IDE集成层:AI增强的编码体验

IDE集成层是开发者接触最频繁的工具层。它们深度集成到开发环境,提供实时的AI辅助。

2.2.1 GitHub Copilot:开发生态的颠覆者

GitHub Copilot是最早大规模商用的AI编程助手,也是目前市场占有率最高的工具。

技术架构

GitHub Copilot架构:

IDE (VS Code/JetBrains/Vim/Neovim)
    ↓ 上下文信息
Copilot Extension
    ├─ 代码上下文提取(当前文件、光标位置、相关文件)
    ├─ 代码风格学习(项目特定的命名习惯、模式)
    └─ 用户习惯学习(常用API、个人偏好)
    ↓ HTTP请求
GitHub Copilot Service
    ├─ 上下文处理
    ├─ Prompt构建
    └─ 缓存优化
    ↓ API调用
OpenAI Codex Model
    ├─ 代码生成
    └─ 多候选生成
    ↓ 响应
Suggestion Ranking & Filtering
    ├─ 安全过滤(避免生成漏洞代码)
    ├─ 质量评分
    └─ 个性化排序
    ↓
IDE展示建议

核心能力详解

1. 实时代码补全

// 场景1:根据注释生成代码
// 计算购物车总价,包含折扣逻辑
function calculateCartTotal(cart: Cart): number {
  // Copilot生成的代码:
  const subtotal = cart.items.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);
  
  const discount = cart.discountCode 
    ? applyDiscount(subtotal, cart.discountCode)
    : 0;
    
  return subtotal - discount;
}

// 场景2:根据函数名生成实现
function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  // Copilot生成的代码:
  let timeout: ReturnType<typeof setTimeout> | null = null;
  
  return function executedFunction(...args: Parameters<T>) {
    const later = () => {
      timeout = null;
      func(...args);
    };
    
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// 场景3:模式识别和补全
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 开发者输入 fetchUser(userId) 后,Copilot自动建议:
    fetchUser(userId)
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Failed to fetch user:', error);
        setLoading(false);
      });
  }, [userId]);
  
  // ...
}

2. 上下文感知能力

Copilot的上下文感知包括多个维度:

  • 文件内上下文:当前文件的内容、导入的模块、定义的函数
  • 项目上下文:项目使用的框架(React/Vue/Angular)、技术栈、代码风格
  • 跨文件上下文:打开的相关文件、最近修改的文件
  • 用户习惯:个人编码风格、常用API、偏好模式
// 示例:项目使用特定的API风格
// 如果项目中普遍使用axios进行HTTP请求:
import axios from 'axios';

// Copilot会建议axios风格的代码
async function fetchUser(id: string) {
  // 而不是fetch
  const response = await axios.get(`/api/users/${id}`);
  return response.data;
}

3. 聊天功能(Copilot Chat)

开发者:@workspace 解释一下这个函数的作用

Copilot:这个函数是一个防抖(debounce)实现,用于限制函数的执行频率。

工作原理:
1. 当函数被调用时,不立即执行,而是设置一个定时器
2. 如果在等待时间内再次调用,清除之前的定时器,重新开始计时
3. 只有等待时间结束且没有新的调用时,才真正执行函数

使用场景:
- 搜索框输入时的自动补全
- 窗口resize事件的处理
- 表单验证的实时反馈

代码分析:
- 使用泛型T保持原函数的类型签名
- 使用ReturnType<typeof setTimeout>获得准确的类型
- 通过闭包保存timeout状态

使用建议

DO(推荐做法)

  • 使用描述性变量名和注释,帮助Copilot理解意图
  • 审查AI生成的代码,不要直接接受
  • 使用Copilot处理样板代码,保留精力处理核心业务逻辑
  • 通过接受/拒绝建议训练Copilot理解你的偏好

DON'T(避免做法)

  • 不要过度依赖Copilot处理复杂算法或安全敏感代码
  • 不要接受不理解的代码
  • 不要忽视Copilot生成的潜在性能问题
  • 不要在保密项目中使用(代码会被发送到云端)

定价与许可

  • 个人版:10/月或10/月或100/年(免费试用30天)
  • 商业版:$19/用户/月(包含管理功能)
  • 开源项目维护者和学生:免费

2.2.2 Cursor:AI原生编辑器

Cursor不是IDE插件,而是一个完整的AI原生编辑器。它基于VS Code fork,将AI能力深度集成到编辑器的每个环节。

核心功能对比

功能 Cursor GitHub Copilot
代码补全 ⭐⭐⭐⭐⭐(更智能) ⭐⭐⭐⭐
聊天功能 ⭐⭐⭐⭐⭐(内置Composer) ⭐⭐⭐⭐(需要切换窗口)
多文件编辑 ⭐⭐⭐⭐⭐(Composer模式) ⭐⭐(需手动切换)
Agent模式 ⭐⭐⭐⭐⭐(自动执行命令) ⭐⭐(不支持)
代码解释 ⭐⭐⭐⭐⭐(@codebase查询) ⭐⭐⭐
价格 有免费版 需要订阅

四大核心模式详解

1. Tab补全(比Copilot更智能)

Cursor的Tab补全不仅基于当前行,还考虑了更多上下文:

// Cursor能跨行预测
function processUserData(users: User[]) {
  const result = users
    .filter(u => u.isActive)
    .map(u => ({          // 按Tab后,Cursor预测:
      name: u.name,       // 自动补全属性
      email: u.email,
      lastLogin: u.lastLogin
    }))
    .sort((a, b) =>       // 按Tab后,Cursor预测排序逻辑
      new Date(b.lastLogin).getTime() - new Date(a.lastLogin).getTime()
    );
  
  return result;
}

2. Chat模式(Cmd+L)

在编辑器内直接与AI对话,无需切换窗口。

开发者:如何优化这个函数的性能?

Cursor:分析代码后,建议以下优化:

1. 使用Map替代find,时间复杂度从O(n²)降到O(n)
2. 避免重复计算,缓存中间结果
3. 使用Web Workers处理大数据集

需要我帮你实施这些优化吗?

[Apply All] [Apply 1] [Apply 2] [Apply 3] [Cancel]

3. Composer模式(Cmd+I)

这是Cursor的杀手锏功能——AI可以同时编辑多个文件。

开发者:"添加用户认证功能,使用JWT token"

Cursor Composer自动执行:

1. 创建 auth.ts 配置文件
2. 安装 jsonwebtoken 依赖(npm install jsonwebtoken @types/jsonwebtoken)
3. 修改 src/App.tsx 添加AuthProvider
4. 创建 src/components/LoginForm.tsx
5. 创建 src/hooks/useAuth.ts
6. 修改 src/api/client.ts 添加token拦截器
7. 创建 src/middleware/auth.ts
8. 运行测试验证功能

[Accept All Changes] [Review Changes] [Cancel]

Composer的技术实现:

Composer Architecture:

User Intent
    ↓
Intent Understanding
    ├─ 识别需要修改的文件
    ├─ 确定修改顺序(依赖关系分析)
    └─ 生成修改计划
    ↓
File-by-File Modification
    ├─ 读取文件内容
    ├─ 生成修改方案
    ├─ 应用修改(使用diff算法)
    └─ 验证语法正确性
    ↓
Cross-File Consistency Check
    ├─ 检查类型一致性
    ├─ 检查导入/导出关系
    └─ 检查命名一致性
    ↓
Present to User

4. Agent模式(实验性功能)

Agent模式让AI拥有更高的自主性,可以执行命令、读写文件、甚至运行测试。

开发者:"重构整个项目的错误处理,统一使用自定义的AppError类"

Cursor Agent执行:

Step 1: 分析项目结构
  ├─ 扫描所有.ts/.tsx文件
  ├─ 识别所有try-catch块
  └─ 识别所有throw语句

Step 2: 创建AppError类
  ├─ 在src/errors/AppError.ts创建基类
  ├─ 支持错误码、HTTP状态码、元数据
  └─ 使用Zod进行运行时类型检查

Step 3: 逐个文件重构
  ├─ src/api/users.ts: 替换3处
  ├─ src/api/orders.ts: 替换5处
  ├─ src/components/ErrorBoundary.tsx: 更新错误处理逻辑
  └─ ...共修改42个文件

Step 4: 运行测试
  ├─ 执行npm test
  ├─ 发现3个失败的测试用例
  └─ 自动修复测试用例

Step 5: 生成总结报告
  ├─ 修改统计:42个文件,156处替换
  ├─ 潜在风险:2处需要人工审查
  └─ 性能影响:无显著影响

[Accept All] [Review Changes] [Run Tests Again] [Rollback]

Cursor的@符号魔法

Cursor提供了强大的上下文引用能力:

@file:src/components/Button.tsx  - 引用特定文件
@folder:src/hooks               - 引用整个文件夹
@codebase                       - 引用整个代码库
@web:React hooks best practices - 搜索网络资料
@docs:project-guidelines        - 引用项目文档

示例:
"@file:src/types.ts 根据这里的类型定义,@file:src/api/client.ts 添加对应的API调用函数"

定价策略

  • Hobby版:免费(每月100次慢速请求,500次Tab补全)
  • Pro版:$20/月(无限快速请求,无限Tab补全)
  • Business版:$40/用户/月(团队协作功能)

2.2.3 Windsurf:Agentic IDE的先行者

Windsurf(原Codeium)提出了"Agentic IDE"的概念,强调AI Agent的自主性。

Cascade多Agent架构

Windsurf的核心创新是Cascade——一个多Agent协作系统:

Cascade Architecture:

Orchestrator Agent(编排器)
    ├─ 理解用户意图
    ├─ 分解任务为子任务
    ├─ 协调其他Agent
    └─ 监控执行进度
    ↓
┌──────────────┬──────────────┬──────────────┐
│  Plan Agent  │  Code Agent  │ Review Agent │
│  (规划)     │  (编码)     │  (审查)     │
└──────────────┴──────────────┴──────────────┘
    ↓
Execution Engine
    ├─ 文件系统操作
    ├─ 命令执行
    ├─ 代码搜索
    └─ 测试运行

实际使用场景

用户:"实现一个完整的用户管理系统,包括注册、登录、权限控制"

Cascade执行过程:

Phase 1: 需求分析(Plan Agent)
  ├─ 识别需要实现的功能点
  ├─ 确定技术栈(从项目配置推断)
  ├─ 生成实施计划
  └─ 输出:任务列表和依赖关系图

Phase 2: 架构设计(Plan Agent)
  ├─ 设计数据库schema
  ├─ 设计API接口
  ├─ 设计组件结构
  └─ 输出:架构文档和数据流图

Phase 3: 并行开发(Code Agent × 多个)
  ├─ Agent A: 实现数据库模型和迁移
  ├─ Agent B: 实现API路由和控制器
  ├─ Agent C: 实现前端页面和组件
  └─ Agent D: 实现认证和授权逻辑

Phase 4: 集成测试(Review Agent)
  ├─ 检查接口一致性
  ├─ 运行单元测试
  ├─ 检查安全漏洞
  └─ 生成测试报告

Phase 5: 优化建议(Review Agent)
  ├─ 性能优化建议
  ├─ 代码质量评分
  └─ 可维护性评估

总耗时:约15分钟(人工开发通常需要2-3天)

Windsurf的独特功能

  1. Supercomplete(超级补全)

    • 不仅补全代码,还补全整个函数、甚至多文件修改
    • 基于项目上下文的深度理解
  2. Explain(代码解释)

    选中一段代码,Windsurf会生成详细的解释:
    - 这段代码的功能是什么
    - 使用了哪些设计模式
    - 可能的性能影响
    - 潜在的改进点
    
  3. Refactor(智能重构)

    • 自动识别代码坏味道
    • 提供重构方案并自动实施
    • 确保重构后行为一致

定价

  • 免费版:基础功能,有限使用次数
  • Pro版:$12/月,无限使用
  • Teams版:$20/用户/月

2.2.4 IDE层工具选型建议

如果你重视代码补全质量:Cursor > GitHub Copilot > Windsurf 如果你需要多文件编辑:Cursor Composer > Windsurf Cascade > Copilot 如果你预算有限:Windsurf免费版 或 Cursor Hobby版 如果你是团队使用:GitHub Copilot Business(管理功能最全)

推荐组合

  • 个人开发者:Cursor Pro(主力)+ GitHub Copilot(备用)
  • 小型团队:Cursor Business + GitHub Copilot Business
  • 大型企业:GitHub Copilot Enterprise(合规性最好)

2.3 设计转代码层:从视觉到实现的跨越

设计转代码工具试图弥合设计师和开发者之间的鸿沟。它们可以将设计稿、截图甚至自然语言描述转换为可运行的代码。

2.3.1 v0.dev:Vercel的AI UI生成器

v0.dev是Vercel推出的AI UI生成工具,它基于Tailwind CSS和shadcn/ui组件库,能够根据自然语言描述生成可交互的React组件。

技术架构解析

v0.dev技术栈:

用户输入层
    ├─ 自然语言描述
    ├─ 参考图片上传
    └─ 交互式迭代对话
    ↓
意图理解层
    ├─ LLM解析需求
    ├─ 提取关键要素:
    │   ├─ 组件类型(表单、表格、卡片等)
    │   ├─ 功能需求(搜索、分页、筛选等)
    │   ├─ 视觉风格(现代、极简、企业级等)
    │   └─ 技术约束(React、TypeScript等)
    ↓
设计系统匹配层
    ├─ 从shadcn/ui选择基础组件
    ├─ 应用Tailwind CSS设计Tokens
    └─ 生成主题配置
    ↓
代码生成层
    ├─ 生成组件结构
    ├─ 实现交互逻辑
    ├─ 添加类型定义
    └─ 优化代码风格
    ↓
预览与迭代层
    ├─ 实时渲染预览
    ├─ 支持交互操作
    └─ 对话式修改

为什么v0选择shadcn/ui + Tailwind CSS?

这个技术栈选择非常有代表性:

1. Tailwind CSS:AI友好的样式方案

<!-- 传统CSS(AI难以理解) -->
<style>
  .user-card {
    padding: 1rem;
    background-color: #f3f4f6;
    border-radius: 0.5rem;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  }
</style>

<!-- Tailwind CSS(AI容易理解和生成) -->
<div class="p-4 bg-gray-100 rounded-lg shadow-sm">

Tailwind的原子化类名具有以下特点:

  • 语义明确p-4表示padding 1rem,比padding: 1rem更易被AI理解
  • 组合性强:通过组合类名实现复杂样式,类似编程中的函数组合
  • 一致性:设计系统被编码在类名中(如text-smtext-basetext-lg
  • 无需命名:不需要为样式起类名,减少了AI的决策负担

2. shadcn/ui:无头组件库的优势

// shadcn/ui组件结构
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"

const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPrimitive.Portal>
    <DialogPrimitive.Overlay className="fixed inset-0 bg-black/50" />
    <DialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
        "bg-white rounded-lg shadow-lg p-6",
        className
      )}
      {...props}
    >
      {children}
      <DialogPrimitive.Close className="absolute right-4 top-4">
        <X className="h-4 w-4" />
      </DialogPrimitive.Close>
    </DialogPrimitive.Content>
  </DialogPrimitive.Portal>
))

shadcn/ui的特点:

  • 无头组件:提供逻辑,不提供样式,样式完全可定制
  • Radix UI基础:基于成熟的headless UI库,可访问性良好
  • 代码即组件:组件代码直接复制到项目,而非通过npm安装
  • TypeScript优先:完整的类型定义

v0.dev的实际使用流程

Step 1: 输入需求
用户:"创建一个用户管理表格,包含搜索、分页和筛选功能,
深色主题,现代简洁风格"

Step 2: v0生成初稿(约10秒)
├─ 生成Table组件
├─ 集成Pagination组件
├─ 添加Search输入框
├─ 实现筛选Dropdown
├─ 应用深色主题
└─ 生成模拟数据

Step 3: 交互预览
├─ 用户可以在预览中交互
├─ 测试搜索功能
├─ 测试分页功能
└─ 查看响应式效果

Step 4: 迭代优化
用户:"搜索框放到右侧,添加一个'新增用户'按钮"
v0:实时更新预览

Step 5: 获取代码
├─ 一键复制代码
├─ 支持导出为Next.js项目
└─ 自动安装依赖指引

生成的代码示例

import { useState } from 'react';
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

interface User {
  id: string;
  name: string;
  email: string;
  role: string;
  status: 'active' | 'inactive';
}

export function UserManagementTable() {
  const [searchQuery, setSearchQuery] = useState('');
  const [selectedRole, setSelectedRole] = useState<string>('all');
  
  // AI生成的模拟数据
  const users: User[] = [
    { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin', status: 'active' },
    { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User', status: 'active' },
    // ...更多数据
  ];
  
  // AI生成的筛选逻辑
  const filteredUsers = users.filter(user => {
    const matchesSearch = user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
                         user.email.toLowerCase().includes(searchQuery.toLowerCase());
    const matchesRole = selectedRole === 'all' || user.role === selectedRole;
    return matchesSearch && matchesRole;
  });
  
  return (
    <div className="w-full max-w-6xl mx-auto p-6 space-y-4">
      {/* AI生成的工具栏布局 */}
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-4">
          <Input
            placeholder="Search users..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            className="w-64"
          />
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button variant="outline">
                Role: {selectedRole === 'all' ? 'All' : selectedRole}
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent>
              <DropdownMenuItem onClick={() => setSelectedRole('all')}>
                All Roles
              </DropdownMenuItem>
              <DropdownMenuItem onClick={() => setSelectedRole('Admin')}>
                Admin
              </DropdownMenuItem>
              <DropdownMenuItem onClick={() => setSelectedRole('User')}>
                User
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </div>
        <Button>Add User</Button>
      </div>
      
      {/* AI生成的表格 */}
      <div className="border rounded-lg">
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Name</TableHead>
              <TableHead>Email</TableHead>
              <TableHead>Role</TableHead>
              <TableHead>Status</TableHead>
              <TableHead>Actions</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {filteredUsers.map((user) => (
              <TableRow key={user.id}>
                <TableCell className="font-medium">{user.name}</TableCell>
                <TableCell>{user.email}</TableCell>
                <TableCell>{user.role}</TableCell>
                <TableCell>
                  <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
                    user.status === 'active' 
                      ? 'bg-green-100 text-green-800' 
                      : 'bg-gray-100 text-gray-800'
                  }`}>
                    {user.status}
                  </span>
                </TableCell>
                <TableCell>
                  <Button variant="ghost" size="sm">Edit</Button>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </div>
    </div>
  );
}

v0.dev的局限性

  1. 可访问性缺失:生成的代码往往需要人工补充aria属性
  2. 业务逻辑空白:只生成UI,不生成API调用和业务逻辑
  3. 复杂交互限制:对于复杂的状态管理和动画,能力有限
  4. 设计系统锁定:必须使用shadcn/ui,迁移到其他组件库需要大量修改

2.3.2 Screenshot-to-Code:开源的视觉转代码标杆

Screenshot-to-Code是GitHub上68,000+ Star的开源项目,由Abi Raja开发。它可以将截图或Figma设计稿转换为代码,支持7种技术栈。

技术架构深度解析

Screenshot-to-Code架构:

输入层
    ├─ 图片上传(PNG/JPG)
    ├─ Figma URL导入
    └─ 视频上传(实验性)
    ↓
视觉解析层(Vision Parser)
    ├─ 多模态模型(GPT-4V/Claude 3/Gemini 2.5 Pro)
    ├─ 分析内容:
    │   ├─ 布局结构(Flex/Grid/Positioning)
    │   ├─ 组件识别(Button/Input/Card等)
    │   ├─ 样式提取(Color/Typography/Spacing)
    │   ├─ 图片检测(需要提取的资源)
    │   └─ 文本内容(OCR提取)
    ↓
布局还原层(Layout Engine)
    ├─ 计算元素位置和尺寸
    ├─ 识别父子关系和层级
    ├─ 推断布局策略
    └─ 生成DOM结构
    ↓
代码生成层(Code Generator)
    ├─ 技术栈选择(React/Vue/Angular/HTML等)
    ├─ 样式方案选择(Tailwind/Inline CSS/CSS Modules)
    ├─ 生成组件代码
    └─ 优化代码结构
    ↓
迭代优化层(Refinement)
    ├─ 多模型并行生成(2个变体)
    ├─ 用户选择和反馈
    └─ 对话式微调

多模型并行生成策略

这是Screenshot-to-Code的核心创新之一:

并行生成流程:

用户上传图片
    ↓
[Thread 1]              [Thread 2]
GPT-4 Vision            Claude 3 Opus
    ↓                       ↓
生成代码变体A          生成代码变体B
(注重精确度)          (注重语义化)
    ↓                       ↓
    └──────────┬──────────┘
               ↓
          展示给用户
               ↓
    ┌──────────┴──────────┐
选择变体A              选择变体B
    ↓                       ↓
系统学习偏好          系统学习偏好
(后续优先使用      (后续优先使用
 GPT-4)              Claude)

支持的7种技术栈

  1. HTML + Tailwind CSS(最常用)
  2. React + Tailwind CSS
  3. Vue + Tailwind CSS
  4. Angular
  5. Svelte
  6. React + Bootstrap
  7. Plain HTML + CSS

实际使用案例

场景:将一个登录页面截图转换为代码

输入:Login Page Screenshot
    ├─ Logo(顶部居中)
    ├─ Email输入框
    ├─ Password输入框
    ├─ "Remember me"复选框
    ├─ "Forgot password?"链接
    ├─ "Sign In"按钮
    └─ "Don't have an account? Sign up"链接

处理过程:
1. GPT-4V分析图片,提取所有视觉元素
2. Layout Engine推断:垂直居中布局,表单宽度约400px
3. Code Generator生成React代码:

```tsx
export default function LoginPage() {
  return (
    <div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        {/* Logo */}
        <div className="text-center">
          <img className="mx-auto h-12 w-auto" src="/logo.svg" alt="Company" />
          <h2 className="mt-6 text-3xl font-extrabold text-gray-900">
            Sign in to your account
          </h2>
        </div>
        
        {/* Form */}
        <form className="mt-8 space-y-6" action="#" method="POST">
          <div className="rounded-md shadow-sm -space-y-px">
            <div>
              <label htmlFor="email-address" className="sr-only">
                Email address
              </label>
              <input
                id="email-address"
                name="email"
                type="email"
                autoComplete="email"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="Email address"
              />
            </div>
            <div>
              <label htmlFor="password" className="sr-only">
                Password
              </label>
              <input
                id="password"
                name="password"
                type="password"
                autoComplete="current-password"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="Password"
              />
            </div>
          </div>

          <div className="flex items-center justify-between">
            <div className="flex items-center">
              <input
                id="remember-me"
                name="remember-me"
                type="checkbox"
                className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
              />
              <label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
                Remember me
              </label>
            </div>

            <div className="text-sm">
              <a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">
                Forgot your password?
              </a>
            </div>
          </div>

          <div>
            <button
              type="submit"
              className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              Sign in
            </button>
          </div>
        </form>
        
        {/* Footer */}
        <p className="mt-2 text-center text-sm text-gray-600">
          Don't have an account?{' '}
          <a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">
            Sign up
          </a>
        </p>
      </div>
    </div>
  );
}

精度评估

根据项目文档和用户反馈:

  • 布局还原度:90-95%(像素级精确)
  • 颜色匹配度:95%+(使用Tailwind最接近的颜色)
  • 字体匹配度:85%(依赖系统字体)
  • 交互功能:30%(仅静态HTML,无JS逻辑)
  • 可访问性:60%(需要人工补充aria属性)

Screenshot-to-Code的局限

  1. 静态代码:生成的代码是静态HTML,没有交互逻辑
  2. 图片资源:无法自动提取和上传图片资源
  3. 响应式:主要还原截图的特定尺寸,其他尺寸需要手动调整
  4. 复杂动画:无法还原复杂的CSS动画和过渡效果

2.3.3 设计转代码层工具对比

工具 开源 技术栈支持 交互生成 迭代能力 价格
v0.dev React only 基础 免费+付费
Screenshot-to-Code 7种 免费
Galileo AI React/HTML 基础 付费
Uizard React/HTML 付费
Anima React/Vue/Angular 付费

选型建议

  • 快速原型:v0.dev(质量最高)
  • 设计还原:Screenshot-to-Code(免费且开源)
  • 团队协作:Figma-to-Code插件(与Figma工作流集成)

2.4 全栈生成层:从想法到应用的一站式体验

如果说IDE集成层是"辅助开发",设计转代码层是"生成UI",那么全栈生成层则是"生成完整应用"。这一层的工具不仅可以生成前端代码,还能处理后端逻辑、数据库、部署等全流程。

2.4.1 Bolt.new:WebContainer技术的革命

Bolt.new是StackBlitz团队推出的AI开发环境,自2024年9月发布以来迅速获得16,000+ Star。它的核心创新是WebContainer技术——在浏览器内运行完整Node.js环境,实现了真正的"零配置即时开发"。

WebContainer技术深度解析

什么是WebContainer?

WebContainer是StackBlitz开发的一项革命性技术,它允许在浏览器中运行完整的Node.js运行时环境。这不是模拟或转译,而是真正的Node.js在浏览器中运行。

WebContainer架构:

传统开发环境:              WebContainer环境:
┌─────────────┐           ┌─────────────────────┐
│   本地OS     │           │      浏览器          │
│  ┌───────┐  │           │  ┌───────────────┐  │
│  │Node.js│  │           │  │  WebContainer  │  │
│  │├─V8  │  │           │  │  ├─Node.js运行时│  │
│  │├─libuv│  │           │  │  ├─文件系统    │  │
│  │├─npm │  │           │  │  ├─npm/yarn   │  │
│  │└─... │  │           │  │  ├─Dev Server │  │
│  └───────┘  │           │  │  └─Terminal    │  │
└─────────────┘           │  └───────────────┘  │
                          └─────────────────────┘
                                  ↑
                            浏览器安全沙箱

技术实现原理

  1. WebAssembly编译:将Node.js核心模块编译为WebAssembly,在浏览器中运行
  2. 虚拟文件系统:在浏览器内存中模拟完整的文件系统,支持读写操作
  3. 进程模拟:使用Web Workers模拟Node.js的多进程能力
  4. 网络拦截:拦截网络请求,模拟HTTP/HTTPS服务端能力
// WebContainer核心API示例
import { WebContainer } from '@webcontainer/api';

// 启动WebContainer实例
const webcontainer = await WebContainer.boot();

// 挂载文件系统
await webcontainer.mount({
  'package.json': {
    file: {
      contents: JSON.stringify({
        name: 'my-app',
        dependencies: { 'next': 'latest' }
      })
    }
  },
  'pages/index.js': {
    file: {
      contents: 'export default function Home() { return <h1>Hello</h1>; }'
    }
  }
});

// 安装依赖
const installProcess = await webcontainer.spawn('npm', ['install']);
installProcess.output.pipeTo(new WritableStream({
  write(data) { console.log(data); }
}));

// 启动开发服务器
const devProcess = await webcontainer.spawn('npm', ['run', 'dev']);

// 监听端口
webcontainer.on('port', (port, url) => {
  console.log(`Server ready at ${url}`);
});

WebContainer vs 传统方案对比

特性 本地Node.js 云端虚拟机 WebContainer
启动时间 秒级 分钟级 毫秒级
网络依赖 需要网络 强依赖 离线可用
资源占用 低(浏览器沙箱)
安全性 依赖系统安全 依赖云端隔离 浏览器安全沙箱
成本 免费 按量付费 免费(客户端运行)
可分享性 需要环境配置 需要账号权限 URL即可分享
Bolt.new的AI集成

Bolt.new将WebContainer与AI深度集成,实现了"对话式全栈开发":

Bolt.new工作流程:

用户输入:"创建一个待办事项应用,使用Next.js和Prisma"
    ↓
AI理解需求
    ├─ 识别技术栈:Next.js + React + TypeScript
    ├─ 识别数据库:Prisma + SQLite
    ├─ 识别功能:CRUD操作、状态管理
    └─ 生成项目结构和文件清单
    ↓
生成代码文件
    ├─ package.json(依赖配置)
    ├─ prisma/schema.prisma(数据模型)
    ├─ src/app/page.tsx(主页面)
    ├─ src/components/TodoList.tsx(组件)
    ├─ src/lib/prisma.ts(数据库客户端)
    └─ API路由文件
    ↓
WebContainer执行
    ├─ 挂载文件到虚拟文件系统
    ├─ 运行npm install(在浏览器中!)
    ├─ 运行prisma migrate(创建数据库)
    ├─ 启动Next.js开发服务器
    └─ 在iframe中展示预览
    ↓
实时预览和迭代
    ├─ 用户查看运行中的应用
    ├─ 用户提出修改:"添加分类功能"
    └─ AI理解、生成代码、热更新

实际案例演示

场景:开发一个博客系统

用户:"创建一个博客应用,功能包括:
1. 文章列表展示
2. 文章详情页
3. 评论功能
4. 使用Markdown写文章
5. 暗色主题支持"

Bolt.new执行过程(总计约3分钟):

[00:00-00:30] 项目初始化
├─ 创建Next.js 14项目(App Router)
├─ 配置TypeScript
├─ 安装依赖:
│   ├─ next@14
│   ├─ react@18
│   ├─ @tailwindcss/typography(Markdown样式)
│   ├─ react-markdown(Markdown渲染)
│   ├─ gray-matter(Frontmatter解析)
│   └─ date-fns(日期格式化)
└─ 配置Tailwind CSS和暗色模式

[00:30-01:30] 核心功能实现
├─ 创建文件系统:
│   ├─ app/page.tsx(文章列表)
│   ├─ app/posts/[slug]/page.tsx(文章详情)
│   ├─ components/PostCard.tsx(文章卡片)
│   ├─ components/CommentSection.tsx(评论组件)
│   ├─ lib/posts.ts(文章数据获取)
│   └─ content/posts/(Markdown文章目录)
├─ 实现功能:
│   ├─ 读取Markdown文件
│   ├─ 解析Frontmatter(标题、日期、标签)
│   ├─ 渲染Markdown内容
│   ├─ 评论提交和展示
│   └─ 暗色模式切换
└─ 添加示例文章

[01:30-02:30] 样式和优化
├─ 设计暗色主题配色
├─ 响应式布局优化
├─ 添加加载动画
├─ 优化字体和排版
└─ 添加SEO元数据

[02:30-03:00] 部署准备
├─ 配置Vercel部署
├─ 生成部署链接
└─ 提供一键部署按钮

结果:
✓ 可运行的博客应用
✓ 在线预览URL
✓ 可下载源代码
✓ 一键部署到Vercel

Bolt.new的技术优势

  1. 真正的即时开发

    • 无需安装Node.js
    • 无需配置开发环境
    • 打开浏览器即可开始
    • 适合教学、演示、快速原型
  2. 完整的开发体验

    • 终端访问(npm、git等命令)
    • 文件系统操作
    • 开发服务器运行
    • 热更新(HMR)
  3. AI深度集成

    • 理解自然语言需求
    • 生成完整项目结构
    • 自动安装依赖
    • 自动运行和调试
    • 对话式迭代修改
  4. 一键部署

    • 直接部署到Vercel、Netlify
    • 生成可分享的URL
    • 支持自定义域名

Bolt.new的局限性

  1. 性能限制

    • 浏览器内存限制(通常<4GB)
    • 大型项目可能运行缓慢
    • 不适合计算密集型任务
  2. 功能限制

    • 无法访问本地文件系统
    • 某些原生模块无法使用
    • 数据库限于SQLite(文件型)
  3. 网络依赖

    • 首次加载需要下载WebContainer运行时
    • npm包需要从registry下载
    • 离线功能有限

适用场景

  • ✅ 教学和学习(零配置环境)
  • ✅ 快速原型验证
  • ✅ 代码演示和分享
  • ✅ 面试编程测试
  • ❌ 大型企业级项目
  • ❌ 高性能计算需求
  • ❌ 本地资源依赖型项目

2.4.2 Lovable:面向非技术用户的AI开发平台

Lovable(原名GPT Engineer)定位为"AI软件工程师",它更进一步,让非技术用户也能创建应用。

产品定位分析

目标用户群体:
├─ 产品经理(快速验证想法)
├─ 设计师(将设计转化为应用)
├─ 创业者(MVP开发)
├─ 小型企业主(内部工具)
└─ 非技术背景的个人用户

核心卖点:
├─ 无需编写代码
├─ 自然语言描述需求
├─ 全流程自动化(设计→开发→部署)
├─ 可视化编辑和迭代
└─ 一键发布上线

工作流程

Step 1: 需求对话
用户:"我想做一个记账应用,可以记录收入和支出,
       按分类统计,有图表展示"

Lovable AI:
├─ 追问澄清:"需要多用户支持吗?"
├─ 追问澄清:"需要什么类型的图表?"
├─ 追问澄清:"需要数据导出功能吗?"
└─ 生成需求文档

Step 2: 技术方案
Lovable AI:
├─ 推荐技术栈:React + Tailwind + Recharts
├─ 推荐数据库:Firebase(简单易用)
├─ 展示原型设计
└─ 用户确认

Step 3: 自动生成
Lovable AI:
├─ 生成项目结构
├─ 生成所有组件代码
├─ 配置数据库连接
├─ 实现认证(如需要)
└─ 生成测试数据

Step 4: 可视化编辑
用户:
├─ 查看实时预览
├─ 拖拽调整布局
├─ 点击修改文案
├─ 选择更换配色
└─ 对话式功能调整

Step 5: 一键部署
Lovable:
├─ 自动构建优化
├─ 部署到云端
├─ 生成可访问的URL
├─ 配置自定义域名(可选)
└─ 提供后续维护支持

与Bolt.new的区别

维度 Bolt.new Lovable
目标用户 开发者 非技术用户
交互方式 代码为主,AI辅助 自然语言+可视化
技术栈 用户指定 AI推荐+用户选择
自定义程度 高(可编辑所有代码) 中(模板+配置)
部署 多平台选择 一体化托管
价格 免费(基础功能) 付费(按项目)

市场影响分析

Lovable代表了一种新的趋势——"无代码+AI"的结合:

传统无代码平台的问题:
├─ 灵活性受限(只能拖拽预设组件)
├─ 学习曲线陡峭(需要理解平台逻辑)
├─ 扩展困难(超出平台能力就无法实现)
└─ 性能问题(生成的代码质量不高)

AI增强的无代码平台:
├─ 灵活性提升(自然语言描述任意功能)
├─ 学习曲线平缓(对话式交互)
├─ 扩展性强(AI可以生成自定义代码)
└─ 代码质量改善(AI生成的代码越来越高质量)

长期影响:
├─ 简单应用开发完全 democratized(民主化)
├─ 专业开发者专注复杂系统和创新
├─ 外包市场萎缩(简单需求被AI满足)
└─ "产品经理+AI"可以替代初级开发者

2.4.3 全栈生成层工具对比

工具 技术栈 数据库支持 部署能力 目标用户 价格
Bolt.new 任意(浏览器运行) SQLite Vercel/Netlify 开发者 免费+付费
Lovable React为主 Firebase/Supabase 托管部署 非技术用户 付费
Replit Agent 多语言 ReplitDB Replit托管 学习者 免费+付费
V0.dev Full Next.js 任意(需配置) Vercel 开发者 免费+付费

选型建议

  • 开发者快速原型:Bolt.new
  • 非技术用户:Lovable
  • 教学场景:Replit Agent
  • Vercel生态:v0.dev

2.5 运行时层:Vercel AI SDK的深度解析

如果说其他工具是"AI辅助开发",Vercel AI SDK则是"AI原生开发"的基础设施。它提供了将AI能力集成到前端应用的完整技术栈。

2.5.1 Provider抽象:统一多模型的架构设计

问题背景

不同的AI供应商(OpenAI、Anthropic、Google等)有不同的API格式和参数,切换供应商需要大量修改代码。

// 直接使用OpenAI API(供应商锁定)
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const completion = await openai.chat.completions.create({
  model: 'gpt-4',
  messages: [{ role: 'user', content: 'Hello' }],
});

// 如果要切换到Anthropic,需要完全重写这部分代码
// API格式、参数名、响应结构都不同

Vercel AI SDK的解决方案

Vercel AI SDK提供了统一的Provider抽象层,通过四层消息架构实现跨模型供应商的无缝切换。

四层消息架构(4-Level Message Architecture):

┌─────────────────────────────────────────────────────────────┐
│  Layer 4: UI Messages (前端渲染层)                           │
│  - 用于React/Vue/Angular/Svelte组件渲染                     │
│  - 包含text、reasoning、tool、file等Part类型                │
│  - 支持渐进式流式渲染                                        │
├─────────────────────────────────────────────────────────────┤
│  Layer 3: Model Messages (开发者体验层)                      │
│  - 用户友好的抽象,用于generate/stream调用                  │
│  - 简化的接口设计                                           │
├─────────────────────────────────────────────────────────────┤
│  Layer 2: Language Model Messages (标准化层)                 │
│  - LanguageModelV4接口规范                                  │
│  - 跨Provider稳定的标准格式                                 │
│  - 统一的Tool Calling规范                                   │
├─────────────────────────────────────────────────────────────┤
│  Layer 1: Provider Messages (供应商适配层)                   │
│  - OpenAI/Anthropic/Google等具体API格式                     │
│  - 各供应商特有的参数和格式转换                              │
└─────────────────────────────────────────────────────────────┘

代码示例

// Vercel AI SDK - Provider抽象
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';

// 使用OpenAI
const result1 = await generateText({
  model: openai('gpt-4-turbo'),
  prompt: 'What is the meaning of life?',
});

// 切换到Anthropic(只需要改这一行)
const result2 = await generateText({
  model: anthropic('claude-3-opus-20240229'),
  prompt: 'What is the meaning of life?',
});

// 切换到Google(同样简单)
const result3 = await generateText({
  model: google('gemini-1.5-pro-latest'),
  prompt: 'What is the meaning of life?',
});

// 其他代码完全不变!

支持的Provider(截至2024年):

// 主流供应商
import { openai } from '@ai-sdk/openai';           // OpenAI
import { anthropic } from '@ai-sdk/anthropic';     // Anthropic
import { google } from '@ai-sdk/google';           // Google
import { azure } from '@ai-sdk/azure';             // Azure OpenAI
import { bedrock } from '@ai-sdk/amazon-bedrock';  // AWS Bedrock

// 开源模型
import { ollama } from 'ollama-ai-provider';       // Ollama本地模型
import { mistral } from '@ai-sdk/mistral';         // Mistral AI
import { groq } from '@ai-sdk/groq';               // Groq
import { perplexity } from '@ai-sdk/perplexity';   // Perplexity

// 国内供应商
import { deepseek } from '@ai-sdk/deepseek';       // DeepSeek
import { qwen } from '@ai-sdk/qwen';               // 通义千问

// 自定义Provider
const customProvider = createProvider({
  apiKey: process.env.CUSTOM_API_KEY,
  baseURL: 'https://api.custom.ai/v1',
  // ...其他配置
});

Provider抽象的技术价值

  1. 无供应商锁定:随时切换AI供应商,无需重写业务逻辑
  2. 成本优化:根据不同任务选择性价比最高的模型
  3. 风险分散:某个供应商服务中断时,可快速切换
  4. 实验便利:方便对比不同模型的效果

2.5.2 Streaming架构:实时交互体验的核心

为什么需要Streaming?

传统AI调用是阻塞式的:等待完整响应后才能展示,用户体验差(等待时间长)。

Streaming让AI响应像打字一样实时展示,极大提升用户体验。

对比:

传统方式(阻塞):
用户发送消息 → 等待5秒 → 一次性显示完整回复
(用户感觉卡顿,不知道是否在处理)

Streaming方式(流式):
用户发送消息 → 立即开始显示 → 逐字出现 → 完整回复
(用户感知响应快,有实时反馈)

技术实现

// 服务端:流式生成
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function POST(req: Request) {
  const { messages } = await req.json();
  
  const result = streamText({
    model: openai('gpt-4-turbo'),
    messages,
  });
  
  // 返回流式响应
  return result.toDataStreamResponse();
}
// 客户端:流式消费
import { useChat } from '@ai-sdk/react';

function ChatComponent() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
  
  return (
    <div className="chat-container">
      {/* 消息列表 */}
      {messages.map(message => (
        <div key={message.id} className={`message ${message.role}`}>
          {/* 消息内容逐字显示 */}
          {message.content}
          
          {/* 流式状态指示 */}
          {message.role === 'assistant' && 
           message.status === 'streaming' && (
            <span className="cursor-blink"></span>
          )}
        </div>
      ))}
      
      {/* 输入框 */}
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="输入消息..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading}>
          发送
        </button>
      </form>
    </div>
  );
}

Streaming协议详解

数据传输格式:

1. Server-Sent Events (SSE)
   Content-Type: text/event-stream
   
   data: {"type":"text","content":"Hello"}
   
   data: {"type":"text","content":" world"}
   
   data: {"type":"finish","reason":"stop"}

2. 支持的消息类型
   ├─ text: 文本内容
   ├─ reasoning: 推理过程(如o1模型的思维链)
   ├─ tool_call: 工具调用请求
   ├─ tool_result: 工具调用结果
   ├─ error: 错误信息
   └─ finish: 完成信号

高级Streaming功能

// 1. 带工具调用的流式响应
const result = streamText({
  model: openai('gpt-4-turbo'),
  messages,
  tools: {  // 定义工具
    getWeather: {
      description: '获取天气信息',
      parameters: z.object({
        city: z.string(),
        date: z.string().optional(),
      }),
      execute: async ({ city, date }) => {
        return await fetchWeather(city, date);
      },
    },
  },
  // 工具调用时的回调
  onToolCall: ({ toolCall }) => {
    console.log(`调用工具: ${toolCall.toolName}`);
  },
});

// 2. 对象流式生成(JSON Stream)
const result = streamObject({
  model: openai('gpt-4-turbo'),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    steps: z.array(z.object({
      step: z.number(),
      action: z.string(),
    })),
  }),
  prompt: '生成一个学习计划',
});

// 流式获取部分解析的JSON对象
for await (const partialObject of result.partialObjectStream) {
  console.log(partialObject); 
  // 可能输出: { title: "学习计划" }
  // 然后: { title: "学习计划", description: "为期3个月的学习计划" }
  // 渐进式完善...
}

2.5.3 Tool Calling:连接AI与外部世界的桥梁

什么是Tool Calling?

Tool Calling(工具调用/函数调用)允许AI在生成内容的过程中,调用外部函数来获取数据或执行操作。

这让AI从"只能对话"变为"可以行动"。

使用场景:

用户:"北京今天天气怎么样?"

没有Tool Calling:
AI:"抱歉,我无法获取实时天气信息。"

有Tool Calling:
AI → 调用getWeather工具(city: "北京") → 获取数据
AI:"北京今天晴天,25°C,适合出行。"

基本用法

import { generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

// 定义工具
const weatherTool = tool({
  description: '获取指定城市的天气信息',
  parameters: z.object({
    city: z.string().describe('城市名称,如"北京"、"上海"'),
    date: z.string().optional().describe('日期,格式:YYYY-MM-DD,默认为今天'),
  }),
  execute: async ({ city, date }) => {
    // 调用天气API
    const response = await fetch(
      `https://api.weather.com/v1/current?city=${city}&date=${date || 'today'}`
    );
    return response.json();
  },
});

const calculatorTool = tool({
  description: '执行数学计算',
  parameters: z.object({
    expression: z.string().describe('数学表达式,如"2+2"、"sqrt(16)"'),
  }),
  execute: async ({ expression }) => {
    // 安全计算
    return safeEvaluate(expression);
  },
});

// AI对话中使用工具
const result = await generateText({
  model: openai('gpt-4-turbo'),
  messages: [
    { role: 'user', content: '北京今天天气怎么样?适合穿什么衣服?' }
  ],
  tools: {
    weather: weatherTool,
    calculator: calculatorTool,
  },
  // 最多允许10轮工具调用
  maxToolRoundtrips: 10,
});

console.log(result.text);
// 输出:"北京今天晴天,气温25°C。建议穿短袖加薄外套。"

多工具协作

// 复杂的工具协作场景
const result = await generateText({
  model: openai('gpt-4-turbo'),
  messages: [{ 
    role: 'user', 
    content: '帮我订一张明天北京到上海的机票,要早上出发的' 
  }],
  tools: {
    // 工具1:查询航班
    searchFlights: tool({
      description: '搜索航班',
      parameters: z.object({
        from: z.string(),
        to: z.string(),
        date: z.string(),
        preferredTime: z.enum(['morning', 'afternoon', 'evening']),
      }),
      execute: async (params) => {
        return await flightAPI.search(params);
      },
    }),
    
    // 工具2:获取用户信息(用于自动填充)
    getUserInfo: tool({
      description: '获取当前用户信息',
      parameters: z.object({}),
      execute: async () => {
        return await getCurrentUser();
      },
    }),
    
    // 工具3:预订航班
    bookFlight: tool({
      description: '预订航班',
      parameters: z.object({
        flightId: z.string(),
        passengerInfo: z.object({
          name: z.string(),
          idCard: z.string(),
          phone: z.string(),
        }),
      }),
      execute: async (params) => {
        return await flightAPI.book(params);
      },
    }),
  },
});

// AI会自动:
// 1. 调用getUserInfo获取用户信息
// 2. 调用searchFlights搜索明天早上的航班
// 3. 向用户确认具体航班
// 4. 调用bookFlight完成预订

前端UI中的Tool Calling

// Tool Calling的可视化展示
function ChatWithTools() {
  const { messages, input, handleSubmit } = useChat({
    api: '/api/chat',
  });
  
  return (
    <div>
      {messages.map(message => (
        <div key={message.id}>
          {/* 文本内容 */}
          {message.content && (
            <div className="message-content">{message.content}</div>
          )}
          
          {/* 工具调用展示 */}
          {message.toolCalls?.map(toolCall => (
            <ToolCallCard 
              key={toolCall.toolCallId}
              toolCall={toolCall}
              toolResult={message.toolResults?.find(
                r => r.toolCallId === toolCall.toolCallId
              )}
            />
          ))}
        </div>
      ))}
    </div>
  );
}

// 工具调用卡片组件
function ToolCallCard({ toolCall, toolResult }) {
  return (
    <div className="tool-call-card">
      <div className="tool-header">
        <span className="tool-icon">🔧</span>
        <span className="tool-name">{toolCall.toolName}</span>
        <span className="tool-status">
          {toolResult ? '✓ 完成' : '⏳ 执行中...'}
        </span>
      </div>
      
      <div className="tool-args">
        <details>
          <summary>参数</summary>
          <pre>{JSON.stringify(toolCall.args, null, 2)}</pre>
        </details>
      </div>
      
      {toolResult && (
        <div className="tool-result">
          <details>
            <summary>结果</summary>
            <pre>{JSON.stringify(toolResult.result, null, 2)}</pre>
          </details>
        </div>
      )}
    </div>
  );
}

2.5.4 Vercel AI SDK的生态系统

框架集成

// React
import { useChat, useCompletion, useObject } from '@ai-sdk/react';

// Vue
import { useChat } from '@ai-sdk/vue';

// Svelte
import { useChat } from '@ai-sdk/svelte';

// Angular
import { useChat } from '@ai-sdk/angular';

// Solid
import { useChat } from '@ai-sdk/solid';

高级功能

// 1. 多模态(图片、音频、视频)
const result = await generateText({
  model: openai('gpt-4-vision-preview'),
  messages: [
    {
      role: 'user',
      content: [
        { type: 'text', text: '描述这张图片' },
        { type: 'image', image: new URL('https://example.com/image.jpg') },
      ],
    },
  ],
});

// 2. 嵌入(Embedding)
const { embedding } = await embed({
  model: openai.embedding('text-embedding-3-small'),
  value: '需要向量化的文本',
});

// 3. 图像生成
const { image } = await generateImage({
  model: openai.image('dall-e-3'),
  prompt: '一只猫在太空',
});

// 4. 语音转文字
const { text } = await transcribe({
  model: openai.transcription('whisper-1'),
  audio: audioFile,
});

2.6 技术选型决策框架和实际案例分析

2.6.1 决策框架

面对众多AI工具,如何做出选择?以下是系统化的决策框架。

第一步:明确需求场景

问题清单:
□ 是日常开发还是原型验证?
□ 是个人使用还是团队协作?
□ 是前端开发还是全栈开发?
□ 需要集成到现有项目还是从零开始?
□ 对代码质量的要求是?(探索性/生产级)
□ 团队的技术水平是?(初级/高级)

第二步:评估维度矩阵

维度 权重 评估标准 评分(1-5)
功能匹配度 30% 是否满足核心需求? ⭐⭐⭐⭐⭐
学习成本 20% 上手难度如何? ⭐⭐⭐
生态成熟度 20% 社区活跃度、文档质量 ⭐⭐⭐⭐
成本效益 15% 免费/付费?性价比? ⭐⭐⭐⭐
可迁移性 15% 是否容易迁移? ⭐⭐⭐

第三步:场景化选型指南

场景1:企业级生产项目
├─ IDE:Cursor(代码质量高)
├─ 运行时:Vercel AI SDK(稳定性好)
├─ UI生成:v0.dev(与Next.js配合好)
└─ 避免:Bolt.new(性能限制)

场景2:快速原型验证
├─ 全栈生成:Bolt.new(最快)
├─ UI生成:v0.dev(质量高)
├─ 代码辅助:Copilot(通用)
└─ 部署:Vercel(一键部署)

场景3:教学演示
├─ 环境:Bolt.new(零配置)
├─ 演示:v0.dev(可视化好)
└─ 文档:AI生成(效率高)

场景4:开源项目
├─ IDE:Cursor(免费版够用)
├─ 辅助:GitHub Copilot(开源免费)
└─ 避免:付费工具(成本控制)

2.6.2 实际案例分析

案例:电商后台管理系统

项目背景:
├─ 团队:5人前端团队
├─ 技术栈:Next.js + TypeScript + Tailwind
├─ 周期:3个月
├─ 需求:商品管理、订单管理、用户管理、数据分析
└─ 质量要求:生产级,高可维护性

工具选型决策:

1. 日常开发:Cursor Pro
   理由:
   ├─ Composer模式支持多文件编辑,适合复杂功能
   ├─ 与VS Code生态兼容,团队迁移成本低
   ├─ 代码质量高,适合生产代码
   └─ 成本:$20/人/月,团队$100/月

2. AI功能集成:Vercel AI SDK
   理由:
   ├─ 与Next.js深度集成(同一团队)
   ├─ Provider抽象,避免供应商锁定
   ├─ TypeScript支持好
   └─ 开源免费,无额外成本

3. UI原型:v0.dev
   理由:
   ├─ 生成shadcn/ui组件,与项目技术栈一致
   ├─ 质量高,减少修改工作量
   └─ 免费使用,成本为0

4. 排除:
   ├─ Bolt.new:性能限制,不适合大型项目
   ├─ Lovable:定制化不足
   └─ Windsurf:团队已有Cursor,功能重复

实施效果:
├─ 开发效率提升:40%
├─ Bug数量:持平(质量把控严格)
├─ 团队满意度:高
└─ 总成本:$100/月(可接受)

案例:创业公司MVP开发

项目背景:
├─ 团队:2人(创始人+设计师,均非技术背景)
├─ 需求:验证产品想法,快速上线
├─ 功能:用户注册、内容发布、评论、支付
├─ 时间:2周
└─ 质量要求:可用即可,后续重构

工具选型决策:

1. 全栈开发:Lovable
   理由:
   ├─ 非技术用户友好
   ├─ 全流程自动化,无需懂代码
   ├─ 一键部署上线
   └─ 成本:$50/月,2周使用成本低

2. 辅助验证:Bolt.new
   理由:
   ├─ 快速验证技术可行性
   ├─ 免费使用
   └─ 可以导出代码供后续开发

3. 排除:
   ├─ Cursor:学习曲线陡峭
   ├─ Vercel AI SDK:需要代码能力
   └─ v0.dev:仅生成UI,不解决全栈需求

实施效果:
├─ 2周内完成MVP上线
├─ 成功验证产品想法
├─ 获得种子轮融资
└─ 后续聘请专业团队重构

2.6.3 成本效益分析

AI工具投资回报率(ROI)计算:

假设:
├─ 开发者年薪:$100,000
├─ 工作小时:2,000小时/年
├─ 时薪:$50
├─ AI工具成本:$50/月 = $600/年

场景1:效率提升20%
├─ 节省时间:400小时/年
├─ 节省成本:400 × $50 = $20,000
├─ ROI:($20,000 - $600) / $600 = 3,233%

场景2:效率提升50%
├─ 节省时间:1,000小时/年
├─ 节省成本:1,000 × $50 = $50,000
├─ ROI:($50,000 - $600) / $600 = 8,233%

结论:AI工具的投资回报率极高,即使效率只提升20%,ROI也超过30倍。

小结

第二章详细介绍了AI前端开发的四层工具生态:

  1. IDE集成层:Cursor、Copilot、Windsurf提供实时代码辅助
  2. 设计转代码层:v0.dev、Screenshot-to-Code弥合设计与开发的鸿沟
  3. 全栈生成层:Bolt.new(WebContainer技术)、Lovable实现零配置开发
  4. 运行时层:Vercel AI SDK提供生产级的AI能力集成

技术选型建议:

  • 生产级项目:Cursor + Vercel AI SDK
  • 快速原型:Bolt.new + v0.dev
  • 非技术用户:Lovable
  • 教学演示:Bolt.new

工具的投资回报率极高,建议团队根据自身情况选择合适的工具组合。


下章预告

第三章《范式的跃迁——从组件驱动到意图驱动》将探讨:

  • 组件驱动 vs 意图驱动的代码范式对比
  • 架构层面的三大转变(声明式→生成式、状态驱动→对话驱动、静态→智能)
  • Prompt工程的新角色和最佳实践
  • 意图层(Intent Layer)的出现和影响

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

一、背景:为什么要做这个 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 落地:从模型博弈到工程化的范式转移|得物技术

文 /魏无涯

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

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

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

重磅预告|OpenTiny 亮相 QCon 北京,共话生成式 UI 最新技术思考

QCon 北京 2026 重磅来袭!🚀

OpenTiny 团队受邀亮相 QCon 全球软件开发大会,带来生成式 UI 最新技术实践分享。

在 AI 重构前端开发的浪潮下,界面开发正从 “手写组件” 走向 “自然语言生成”。但模型生成的界面往往难以落地:交互不完整、业务逻辑缺失、无法对接真实后端与工具生态……

本次分享,OpenTiny 团队林瑞虹老师将聚焦 GenUI SDK 这套面向生成式 UI 的前端开发工具,介绍了生成式 UI 的原理以及在业务场景落地诉求下对能力的改造与扩展,讨论了生成式 UI 性能指标以及应用场景的局限性。并对业界多个生成式 UI 产品协议进行对比,探讨了协议标准化的不同观点。

你将听到这些硬核内容

  • 生成式 UI 落地的真实痛点与解决方案探讨
  • GenUI SDK 核心原理设计:如何保证界面可控、可扩展、可维护
  • 业界多协议对比及标准化方向思考

无论你是前端开发者、架构师,还是关注 AI + 前端 的技术负责人,都能在本次分享中清晰理解生成式 UI 的技术价值、实现原理、落地场景与现实局限,为后续技术选型提供扎实参考与决策依据。

活动信息

  • 会议: QCon 北京 2026 全球软件开发大会
  • 专题: 下一代交互架构:LUI 与 GUI 的融合
  • 主题: 生成式 UI :AI 交互新模式探索
  • 讲师: OpenTiny 团队林瑞虹老师

欢迎现场交流,一起探索前端开发的下一代范式。关注我们,后续将分享完整演讲干货。

图片

关于 OpenTiny NEXT

OpenTiny NEXT 是一套企业智能前端开发解决方案,以生成式 UI 和 WebMCP 两大核心技术为基础,对现有传统的 TinyVue 组件库、TinyEngine 低代码引擎等产品进行智能化升级,构建出面向 Agent 应用的前端 NEXT-SDKs、AI Extension、TinyRobot智能助手、GenUI等新产品,实现AI理解用户意图自主完成任务,加速企业应用的智能化改造。

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design
GenUI SDK 代码仓库:github.com/opentiny/ge… (欢迎star ⭐)
关于我们:opentiny.design/opentiny-de…

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~如果你有任何问题,欢迎在评论区留言交流!

Harness Engineering:为什么你用 AI 越用越累?

Harness Engineering:驾驭 AI Agent 的工程学

Harness Engineering 封面图

"任何时候当你发现一个 agent 犯了一个错误,你就花时间工程化地解决它,使得这个 agent 再也不会犯那个错误。" — Mitchell Hashimoto(Terraform / Ghostty 作者,Harness Engineering 早期推广者之一)


换了更好的模型,只提升了 0.7%

LangChain 用一次实验把一件事说清楚了。

他们拿同一个模型参加 Terminal Bench 2.0 基准测试:默认设置跑出 52.8 分,排第 30 名;什么模型参数都没改,只调整了 agent 的运行环境——文档结构、验证回路、追踪系统——分数跳到 66.5,排名升到第 5 名,提升 26%

对比组:换成更好的模型,提升 0.7%

这组数字在工程师圈子里流传了很久。不是因为好看,而是因为它指向一个让人不舒服的问题:如果你的 AI 工程精力都集中在"换更好的模型"上,你可能把 99% 的注意力放在了那 0.7% 的空间里。

这就是 Harness Engineering 要解决的问题。


三次范式跃迁

AI 工程已经走过了三代。每一代工程师的焦点都不一样:

三次范式跃迁图

第一代:Prompt Engineering(2022-2024),问题是"怎么跟模型说话"。Few-shot、Chain-of-Thought、角色设定——工程师花大量时间打磨措辞,因为同一个问题换种说法,结果可能天差地别。

第二代:Context Engineering(2025),瓶颈转移了。影响质量的不再是怎么说,而是给它看什么。私域知识、历史对话、动态状态——怎么把正确的信息在正确的时机送进上下文窗口,成了核心工程问题。

第三代:Harness Engineering(2026 起),瓶颈再次转移。问题不再只是"给 agent 看什么",而是"在什么样的系统里让它工作"——约束、工具、反馈机制、验证回路,以及在 agent 出错时让整个系统能自我修正的能力。

Prompt Engineering  →  优化说话方式
Context Engineering →  优化信息质量  
Harness Engineering →  优化运行系统

OpenAI 在内部实验报告里直接说了:

"早期进展比预期慢,不是因为 Codex 能力不足,而是因为环境设计不充分。Agent 缺少可靠推进目标所需的工具、抽象和内部结构。"


什么是 Harness Engineering?

"Harness" 来自马术——那套套在马身上、用于控制和驾驭的整套装具:笼头、缰绳、胸带、肚带。它不是让你骑马,而是让马在你设计的系统里知道该往哪走、什么时候停、哪里绝对不能踏入。

在 AI agent 的语境里,harness 指的是模型本身以外的一切

AI Agent = 模型 + Harness

包括上下文配置、工具集、约束规则、反馈循环、子 agent 架构——所有让模型在你的具体问题域里可靠工作的工程设施。

这个概念由实践者 Viv 首创,Mitchell Hashimoto 是最早公开使用并推广它的人之一。他给出的定义极其简洁:每当发现一个 agent 犯了错,就把这个错变成物理上不可能再发生的事。不是修 prompt,不是换模型——是工程化地消灭这类失败。

Harness Engineering 不是一个框架,不是一个库,是一套工程实践哲学


这些都不是 PPT 数字

在讨论怎么做之前,先看几个已经在生产里跑的案例:

Peter Steinberger(OpenClaw 作者):一个人,一个月 6600+ commits,同时运行 5-10 个 agent,发布的是自己没有逐行读过的代码。

OpenAI 内部团队:3 名工程师,5 个月,用 Codex 建造了一个百万行的内部产品,零行手写代码(by design)。平均每人每天 3.5 个 PR,吞吐量随团队增长持续提升。

Stripe Minions:内部 coding agent,每周合并超过 1000 个 PR。工程师在 Slack 发任务,agent 写代码、跑 CI、开 PR,全程无需人工干预。

8Lee(YEN 作者):一条命令 $zip,编译、签名、公证一个覆盖 30 种语言的 macOS 桌面应用,15 分钟完成,近 1000 次发布,零次出错。

Anthropic 内部实验:16 个 Claude 实例并行写 C 语言编译器,历经 2000 个 session、两周时间、约两万美元 API 费用,产出了 10 万行编译器代码——能编译出可以正常启动 Linux 的程序。

以上都不是 demo,都是真实规模的生产系统。让它们得以运转的,是各自精心设计的 harness。


越快越慢:AI 的速度陷阱

这里有一组让人不舒服的数字,来自 Harness 的《2026 DevOps 现代化报告》:

在每天频繁使用 AI 工具的重度用户里:

  • 69% 表示 AI 生成的代码会频繁引发部署问题
  • 事故恢复平均时长 7.6 小时,比轻度用户还要长
  • 47% 反映下游的手工工作——QA、验证、修复——比以前更繁重

DORA 的数据从另一个角度印证了同样的问题:AI 让个人生产率提升 19%,但组织吞吐量只提升了 3%,交付稳定性甚至下降了 9%

写代码的速度提升了,但交付系统被暴露了。就像把火车开得更快,但铁路还是按原来的时速设计的——摩擦越来越大,随时有翻车风险。

加速代码生成,不等于加速软件交付。 Harness 是连接两者的桥梁。


模型偷懒:一个比"上下文太长"更深的问题

在讲具体的工程实践之前,有一个反直觉的研究结论值得单独讲清楚,因为它影响了 harness 设计的底层逻辑。

大家都知道上下文太长会影响模型表现。但通常的解释是"模型被搞混了"。Yandex 研究员 Rodionov 的实验推翻了这个假设:

模型不是被搞混了,它是选择了少思考。

他向 Qwen 的上下文里注入 128 个随机 token 的噪音——仅仅 128 个 token。结果:

  • 准确率从 74.5% 降到 67.8%
  • 推理 token 数量从 28,771 降到 16,415,减少了 43%
  • 推理深度下降 18%

更反直觉的:推理能力越强的模型,退化越严重

噪音触发的不是混乱,是懒惰。模型看到上下文质量下降,会主动降低思考投入。

Anthropic 的情感研究团队在模型内部找到了这个现象的神经层面解释:他们发现了一个"desperate(绝望)"情感向量——当它激活时,模型倾向于走捷径、寻找替代路径逃避任务。对应地存在一个"calm(平静)"向量,能抑制这种倾向。

这对 harness 设计有直接影响:上下文管理的核心不只是过滤信息,而是防止信号质量下降触发模型的懒惰机制。你需要保证进入 agent 的每一条信息都是高信噪比的。


Harness Engineering 的六个核心组件

Harness Engineering 六个核心组件图

1. AGENTS.md:写给 AI 的操作手册

大多数项目有 README,但 README 是写给人类的。AGENTS.md(或 CLAUDE.md)是写给 AI 的——每次 agent 启动都会读这个文件。

AGENTS.md 的本质不是描述项目,而是记录历史失败。

Hashimoto 在他的终端模拟器 Ghostty 里观察到:这个文件里的每一行,都对应一次真实发生过的 agent 失败。它不是他预先设计的规则,是他从真实错误里提炼出来的防火墙。

# AGENTS.md(节选自实战案例)

## 代码签名规则
- **绝对不要**使用 `codesign --deep`,它会生成无效的嵌套签名
- 正确的签名顺序是从内到外:先签最内层二进制,最后签外层 app bundle

## Git 操作规则  
- **绝对不要**使用 `git add -A`,除非你刚刚运行了 `git status`
- **绝对不要** force push,除非被明确要求

## 测试规则
- **绝对不要**写只测试 mock 行为的测试
- **绝对不要**因为测试失败就删除测试

写法有数据支撑。 Vlad Temian 做了 150+ 次实验测量 Claude 对指令的遵从率:

写法 遵从率
简洁强硬("NEVER do X") 94.8%
详细解释("Because of reason Y, you should consider not doing X") 86.6%

ETH 苏黎世的研究也发现,大多数 AGENTS.md 文件要么没用,要么有害——主要原因是太长、太模糊、包含条件性规则。让 AI 帮你生成这个文件,实际上会降低性能,还额外消耗 20% 以上的 token。

几条实践原则

  • 总长度控制在 300 行以内(HumanLayer 自己的在 60 行以下)
  • 每条规则一句话,不加解释,不加"因为"
  • 只放普遍适用的规则,条件性规则用技能(Skills)处理
  • 手工写,每次 agent 犯错后更新

2. Hooks:把"告知"变成"拦截"

这是 Harness Engineering 里最反直觉但最有效的洞见:

强制执行远比告知可靠。

写在 AGENTS.md 里的规则,agent 可能在某个复杂的上下文里忽略掉。在命令执行之前拦截它的脚本,agent 物理上无法绕过。

#!/bin/bash
# guard-codesign-deep.sh

if echo "$TOOL_INPUT" | grep -q '\-\-deep'; then
  echo "BLOCKED: codesign --deep 会产生无效的嵌套签名。"
  echo "正确做法:从内到外签名,先签最内层二进制,最后签外层 app。"
  exit 1
fi

这 5 行脚本比任何 prompt 都可靠。不管上下文有多长,不管 prompt 多复杂,agent 永远不会成功执行 codesign --deep

8Lee 为 YEN 项目定义了 5 个 hook,覆盖他认为最危险的失败场景:

Hook 防护目标
block-rm.sh 防止 rm -rf 灾难性删除
guard-force-push.sh 保护 commit 历史
guard-codesign-deep.sh 强制正确的签名顺序
guard-vendor.sh 防止直接修改第三方库
guard-sensitive-file.sh 防止 .env.pem.key 泄露

总投入:约 2 小时。收益:近千次发布零安全事故。


3. 架构即护栏:越相信 AI,越需要给它设限

OpenAI 内部团队在构建百万行产品时得出了一个反直觉的结论:

"Agent 在有严格边界和可预测结构的环境里效率最高。所以我们围绕极度刚性的架构模型构建应用。每个业务域被分成固定的几层,依赖方向经过严格验证,可接受的边集非常有限。这些约束通过自定义 linter(由 Codex 生成)和结构测试机械地强制执行。"

Thoughtworks 的 Birgitta Böckeler 把这个原则概括得很清晰:

提高对 AI 生成代码的信任,需要缩小选择空间,而不是扩大自由度。

  • 架构灵活 → agent 每个决策点都有太多可能性 → 行为不可预测
  • 架构刚性 → agent 每个决策点只有少数合法选项 → 行为可靠

这里有一个工程上的精妙设计:OpenAI 团队的 linter 报错同时包含修复指南

❌ ArchViolation: service-layer 不能直接依赖 repository-layer
   解决方案:通过 domain-service 接口访问,参见 docs/architecture.md#dependency-rules

工具不只在拦截,它在教 agent 下一步该怎么做。


4. Sub-Agent 架构:Context 防火墙与并发控制

Context Rot(上下文腐化)是真实的,而且比你想象的更深

Chroma 测试了 18 个模型,发现随着 context window 长度增加,模型在任务上的表现单调下降——即使是简单任务。当上下文里有低语义相关的干扰项时,下降更陡。

这还有一个更隐蔽的问题:Context Anxiety(上下文焦虑)——部分模型在感知到 context window 快满时,会主动提前收尾、跳过尚未完成的步骤。Agent 不是因为任务完成了才停,而是因为它"感觉快撑不住了"就停了。

结合前文的 Rodionov 研究,上下文问题的全貌是:质量下降触发懒惰,容量耗尽触发焦虑。两者都不是"模型被搞混了",而是模型主动选择了少做

解决方案不是更大的 context window(那只是让稻草堆更大)。是 Sub-Agent 架构:

Main Agent(规划 + 编排,昂贵模型 Opus)
  ├── Sub-Agent A(代码库探索,便宜模型 Haiku)→ 只返回文件路径:行号
  ├── Sub-Agent B(安全审计,便宜模型 Haiku)→ 只返回漏洞列表
  └── Sub-Agent C(依赖分析,便宜模型 Haiku)→ 只返回版本建议

每个 sub-agent 在隔离的 context window 里运行,只有最终浓缩的结果传回主线程。主 agent 的上下文始终保持干净、高信噪比。

并发架构:更进一步

当单个 agent 能稳定工作后,下一个问题是:能不能同时派出一百个去干活?

不能直接堆数量。 Cursor 团队的教训:让几百个 agent 共享一份大型项目,当 20 个 agent 同时工作时,有效吞吐量下降到只相当于两三个 agent。原因是上下文互相污染,加上全局资源的争抢。

成熟的并发架构是三层分工:

Planner(规划器)— 分解任务,分配工作,不写代码
  └── Worker(执行器)× N — 各自在隔离环境里执行
        └── Judge(裁判)— 独立验证,不参与执行

配合 DAG 引擎确保工作单向流动,防止循环依赖。

Anthropic 在并发 agent 里找到了另一个优雅的设计:GAN 启发的 Generator + Evaluator 对抗结构。评估者不只看结果,而是亲自动手验货——打开浏览器、点击页面、验证报错栈,像真实用户一样操作一遍。Generator 和 Evaluator 先协商"做完长什么样",再各自工作,形成对抗性的质量保证。

8Lee 的 $team 技能把这个思路推到了极致:8 个独立 agent 做代码评审,最后一个是 Devil's Advocate(唱反调的),专门挑战其他 7 个 agent 的所有建议。它检查严重性评级、标记假阳性、找矛盾。对抗性自我纠正,内置在 skill 结构里。


5. 长时任务 Harness:失忆实习生问题

长时任务 Harness 结构图

这是很多人没有意识到的一个独立问题。

长时任务的核心挑战:Agent 必须在多个 context window 里工作,而每次新的 session 开始时,它完全不记得之前发生了什么。就像一个软件项目由工程师轮班完成,每个新来的工程师对之前的工作没有任何记忆。

Anthropic 在实验中观察到了两个典型失败模式:

  1. "一口气干完":agent 试图一次性完成所有功能,上下文耗尽后留下半成品,下个 session 花时间重建状态,再从头来
  2. "差不多了":agent 看到一点进展就宣布"完成了",然后停工

他们的解法是双 agent 架构

Initializer Agent(初始化 agent),只在第一次运行时启动,建立:

  • feature_list.json:完整功能列表,每项初始为 "passes": false
  • init.sh:一键启动开发服务器
  • claude-progress.txt:每个 session 都会更新的进度日志
  • 初始 git commit

Coding Agent(编码 agent),后续每次 session 开始时执行固定的三步:

# 三步定位:让 agent 快速了解自己的处境
1. pwd                          # 确认工作目录
2. git log --oneline -20        # 了解最近发生了什么
3. cat claude-progress.txt      # 看上一班留下的进度

然后读取 feature_list.json,选优先级最高的未完成功能,一次只做一个,完成即更新状态并 commit。

一个值得注意的细节:用 JSON,不用 Markdown。实验发现,模型倾向于不当地覆盖 Markdown 文件,对结构化 JSON 则克制得多——它只改 "passes" 字段的值,不会擅自删除条目。

这把每个 coding session 变成了一个纯函数:

f(功能列表 + git 历史 + 进度文件) → 完成一个功能 + 更新记录

6. Skills:按需加载,而不是全部预装

大多数人遇到问题的第一反应是:把所有信息塞进系统提示。

结果是:agent 在看完一万 token 的指令之后,剩下的可用注意力所剩无几。OpenAI 把这叫做"1000 页说明书变成陈旧规则的坟场"。

技能(Skills)的解法是按需披露

  • agent 只在需要某个能力时,才加载对应的技能文档
  • 每个技能是一个目录,包含 SKILL.md 和相关资源
  • 加载时,技能内容作为消息注入当前上下文

8Lee 的实现分三层:

Level 1SKILL.md 封面(~100 tokens)——技能发现,Agent 决定是否需要
Level 2SKILL.md 主体(~800-1000 tokens)——阶段图、协议、所有 guards
Level 3:当前阶段的参考文件(~200-600 tokens)——只加载正在执行的阶段

上下文的消耗量始终与当前任务的复杂度成正比,而不是与整个项目的复杂度成正比。


更完整的分析框架:Feedforward + Feedback

Feedforward 与 Feedback 控制矩阵图

Thoughtworks 的 Birgitta Böckeler 提出了一个系统化的思考框架,把 harness 的所有控制机制划分成两个维度。

维度一:控制方向

Feedforward(前馈控制) — 在 agent 行动之前引导它:AGENTS.md 里的规则、架构约束说明、Skill 里的 how-to 指南。

Feedback(反馈控制) — 在 agent 行动之后感知并纠正:测试结果、Linter 输出、类型检查错误。

只有 Feedforward,agent 知道规则但无法验证自己是否遵守了。只有 Feedback,agent 会反复犯同类错误,因为没有预防。两者缺一不可。

维度二:执行类型

Computational(计算型) — 确定性的,CPU 执行:测试、linter、类型检查、结构分析。毫秒到秒级,结果完全可靠,便宜,可以每次提交都跑。

Inferential(推断型) — 语义分析,LLM 执行:AI 代码评审、"LLM 作裁判"。慢而贵,有不确定性,但能处理需要语义判断的场景。

组合起来:

Feedforward Feedback
Computational 架构边界 linter 结构测试、覆盖率
Inferential AGENTS.md 规则、Skills AI 代码评审

最佳实践是:尽量用 Computational,把 Inferential 留给真正需要语义判断的场景

三类 Harness 目标

可维护性 Harness — 最成熟:重复代码、圈复杂度、测试覆盖率、架构漂移,Computational 工具基本都能覆盖。

架构适应性 Harness — 定义和检查架构特征:性能需求前馈 + 性能测试反馈;可观测性约定 + 日志质量检查。

行为 Harness — 最难,仍是开放问题,但正在取得突破。

传统测试框架在这里遭遇根本性失败:你无法给 LLM 的输出写 assertEquals(expected, actual)——相同问题的"正确回答"可以有无数种表达。更深的矛盾是,生成式 AI 的多样性输出不是 bug,是 feature。

突破口是用 AI 测试 AI:不是比对字符串,而是判断意图。一个 AI judge 向另一个 AI 提问:"用户的登录成功了吗?"而不是"div.login-btn 是否存在?"这个 judge 每次运行时重新分析页面 DOM 和截图,给出带推理说明的判断——而非简单的 pass/fail。

PKU 和 HKU 联合推出的 Claw-Eval 基准测试进一步工程化了评估方法:Pass³ 方法论——一个任务必须在三次独立运行中全部通过才算真正通过,彻底消除"幸运运行"的干扰。同时从三个维度评分:Completion(完成度)、Safety(安全性)、Robustness(鲁棒性)。这是在把evaluation harness 本身工程化。


交付侧的 Harness:黄金标准管道

黄金标准管道图

上面讨论的六个组件主要针对 coding agent 的行为控制。但 Harness Engineering 的边界不止于代码生成——从代码到生产的整个交付管道同样需要 harness 化。

Harness 平台工程师 Aditya Kashyap 提出了一个**黄金标准管道(Golden Standard Pipeline)**的四层架构:

Layer 1:治理域(Governance Domain)
  └── 策略即代码(OPA)在管道执行前作为第一道关卡
  └── 原则:不合规的管道不允许启动

Layer 2:集成域(Integration Domain)——内循环
  └── 代码气味、lint、安全扫描并行而非串行
  └── 原则:安全扫描应该让开发提速,而不是增加摩擦

Layer 3:信任域(Trust Domain)——供应链安全
  └── SBOM(软件物料清单):制品的成分表
  └── SLSA 证明:构建过程的不可伪造 ID
  └── 加密签名(Cosign):数字封印,任何篡改都会破坏

Layer 4:交付域(Delivery Domain)——外循环
  └── 不可变制品:构建一次,部署到处
  └── 滚动部署 + 审批门控

其中最重要的是 Layer 1 的哲学转变:传统管道在快要部署时才做合规检查(浪费了前面 20 分钟的构建时间),黄金标准把治理移到"第零步"——不合规的管道甚至不会开始执行

Layer 3 对应了当前软件供应链安全的核心挑战:你需要能证明"这个制品是在哪台机器上构建的、什么时间、用了哪些输入"。当下一个 Log4j 出现时,SBOM 让你不需要扫描整个世界,只需要查询你的制品库存。


实战:Skill 分类学

不是所有任务都同样脆弱。8Lee 提出了基于脆弱性的技能分类:

高脆弱性任务(签名、部署、安全操作)
  └── Hard Gates + 失败即停 + 无恢复重启
  └── 示例:代码签名、公证、加密操作

中脆弱性任务(质量门控)
  └── Quality Gates + 失败即回滚
  └── 示例:依赖更新、staging 部署

低脆弱性任务(lint、格式化)
  └── 简单 pass/fail
  └── 示例:代码格式化、静态检查

在低风险任务上过度约束,浪费 token。在高风险任务上约束不足,迟早出事。


验证反压:成功静默,失败才说话

HumanLayer 认为,agent 解决问题的成功率与它验证自己工作的能力高度相关。

他们建了完整的验证链路:类型检查 + 构建、Biome 格式化 + lint、Playwright 端到端测试、代码覆盖率(低于阈值时强制补写)。

但有一个容易踩的坑:让 agent 每次修改后跑完整测试套件,4000 行的通过输出会塞满上下文窗口,agent 随之开始产生幻觉。

解决方法很简单:成功时不输出任何东西,只有失败才打印详情。

# 成功无输出,失败才打印——context window 零污染
OUTPUT=$(run_build 2>&1)
if [ $? -ne 0 ]; then
  echo "$OUTPUT" >&2
  exit 1
fi

这条原则在所有成功的 harness 设计里反复出现:信号噪比是 context 管理的核心


真实案例:8Lee 的 $zip 命令

这是目前公开记录最详细的 harness engineering 案例。

一条命令 $zip 触发:
├── 12 个顺序步骤(预检、vendor 门控、版本升级、同步、验证...)
├── 65 个验证检查(13 预构建 + 44 核心 + 8 后构建)
├── 5 个编译器(Zig + Swift + Xcodebuild + Go + swiftc)
├── 签名 + 公证 + DMG 打包 + Supabase 上传
├── Vercel 部署(Next.js 下载页面 + API + SEO 元数据)
└── git commit(含 SHA-256 校验文件)+ 文档更新

耗时:约 15 分钟
发布次数:近 1000 次
失败次数:0

他的结论很直接:

"我不再担心发布的正确性了。不是因为 AI 是完美的,而是因为 harness 让「我们一起在做的事」变得安全。"


Harness 应该越来越薄

大多数讨论都在讲"加什么"。但这个洞见值得单独强调:

"Harness 的每一个组件,都编码了一条关于模型做不到什么的假设。当这个假设不再成立,组件就该走了。"

Anthropic 自己做了这件事。随着 Opus 4.5 和 4.6 发布:

  • Context Reset(上下文重置机制):删掉了。新模型的上下文管理能力已经不需要这个补偿。
  • Sprint Contract(冲刺合约,用于控制 agent 执行节奏的约束):删掉了。新模型能自己把控节奏。

每加一个 harness 组件,都是在补偿"当前模型无法独立完成某件事"。每当模型进步让某个补偿变成负担,就该拆掉它。

这同时意味着:今天一些 harness 组件的必要性,来自当前模型的"懒惰"倾向(如前文 Rodionov 的研究所揭示)。Anthropic 的情感向量研究暗示,未来可能可以在模型内部调节这个状态,而不需要外部 harness 补偿——到那时,对应的组件自然退出。

真正的竞争优势不在 harness 的厚度,而在于追踪这个迁移面的速度——知道下一步该加什么,上一步该拆什么。

johng 把这叫做 Harness Engineering 的第六支柱:可拆卸性(Detachability)——以模块化设计构建 harness,让它能随模型迭代优雅退场,而不是每次模型升级都需要大规模重构。


未来三个阶段

我们不会一夜之间拥有完全自主的 SRE 团队。这个演进以三个浪潮的方式推进。

Horizon 1:增强型运营者(当下)

Agent 是工程师的"副驾"。你问"这个 Pod 为什么崩溃了",agent 查日志、关联 MemoryLimitExceeded 错误和最近的配置变更,提出修复建议。人类创建意图并批准行动。

Harness 重点:AGENTS.md + Hooks + 可观测性集成。

Horizon 2:Agent 群体与任务自主(1-2 年)

单个专业化 agent 开始在特定范围内自主处理重复任务。一个"安全 agent"发现 CVE,创建 ticket 并传给"开发 agent",后者建分支、升版本、传给"QA agent"跑测试。人类只在最后点击"合并"。

从 Human-in-the-Loop 转变为 Human-on-the-Loop——你审查输出,但不驾驶过程。

Harness 重点:多 agent 编排 + Judge 模式 + 严格权限隔离(Diagnosis Agent 只有读权限,Remediation Agent 只有目标命名空间的写权限)。

Horizon 3:自主 SRE(3-5 年)

凌晨 2 点生产延迟飙升,"SRE Agent"检测到异常、识别噪音邻居、驱逐节点、验证稳定性、向 Slack 发送事后分析。只有 agent 无法解决时才呼叫人类。

标准操作的 Human-out-of-the-Loop。人类管理策略和目标,不管任务。

Harness 重点:Constitutional AI(Policy-as-Code 通过 OPA 作为所有工具调用的第一道关卡)+ 防篡改审计日志(记录每个推理步骤和每条 CLI 命令)。

每个阶段的关键认知转变:我们不再管理服务器,我们在管理认知架构(Cognitive Architectures)。


开放的硬问题

Harness Engineering 作为一个工程学科仍然年轻。几个核心问题目前没有答案:

代码质量的慢性退化:agent 生成的代码不以人类的方式腐化——不是有 bug,而是"功能正确但逐渐不可维护"。OpenAI 在跑周期性的"垃圾清理 agent",Anthropic 在跑"Doc-gardening agent"(扫描代码和文档的脱节并发起 PR),但这些实践仍很早期。

用 AI 验证 AI 的可靠性:主要靠 AI 生成的测试来验证 AI 生成的代码,这个闭环的可信度是多少?目前没有答案。

老旧代码库的改造:几乎所有成功案例要么从零开始,要么团队在全新项目里构建 harness。把这些方法应用到有十年历史、测试参差不齐、文档残缺的存量代码库,难度是另一个量级。Böckeler 打了个比方:这就像在从未跑过静态分析的代码库上第一次跑——你会溺死在警报里。

Harness 自身的一致性:随着 harness 增长,前馈规则和反馈信号可能开始互相矛盾。当它们指向不同方向时,agent 如何做出合理权衡?如何衡量 harness 的"覆盖率",就像测试覆盖率一样评估它的完整性?目前没有工具可以回答。

概率性系统的信任问题:脚本是确定性的,同样输入永远得到同样输出。Agent 是概率性的,可能根据上下文选择不同路径。让概率性系统可信赖,答案不是消除不确定性,而是确保全程可追溯——只有能被看见的,才能被信任。


从今天开始做什么

第一周:建立基础

  1. 为你最常用的项目创建 AGENTS.md(或 CLAUDE.md

    • 从当前最烦的 5-10 个 agent 失败行为开始
    • 每个写一条规则,一句话,不加解释
    • 总长度控制在 50-100 行
  2. 让 agent 能操作你的项目

    • 所有日常工作流写成 Makefile target(make devmake testmake restart
    • agent 应该能自己启动项目、看日志、跑测试
  3. 建立最小反馈回路

    • linter + 类型检查 + 单元测试,必须能本地快速跑完
    • 失败时才输出,成功时静默

第二到四周:工程化失败

  1. 识别前 5 个最危险的失败模式,把它们变成 hook 拦截脚本

  2. 如果你有跨多个 session 的长任务,建立 Initializer + Coding Agent 双 agent 模式

    • 用 JSON 跟踪功能状态,不用 Markdown
    • 每次 session 开始强制读进度文件和 git log
    • 每次只完成一个功能,完成即 commit
  3. 第一个技能(Skill)——选一个每周都要做的、有多个步骤的任务

持续运转:把每一次失败变成系统

每次 agent 犯错,问自己:

  • 这是 AGENTS.md 可以防止的?→ 加一条规则
  • 这是 hook 可以物理阻止的?→ 写一个拦截脚本
  • 这是 linter 可以检测的?→ 写一条 lint 规则
  • 这是 sub-agent 可以隔离的 context 问题?→ 拆分架构
  • 这是模型已经能自己处理的?→ 删掉这个 harness 组件

唯一的原则:只在 agent 真的出错后才加约束,只在模型真的不再需要时才删约束。


结语:一门关于信任的工程学

构建自动化的历史,一直在回答同一个问题:如何让复杂的多步骤过程变得可靠和可重复?

1976:make         依赖图 + 文件时间戳
1990s:autotools   跨平台构建
2000s:CI/CD       远程机器运行构建
2010s:IaC         可复现的基础设施
2020s:GitOps      声明式期望状态
2026+:Harness     Agent 读取操作手册并执行,harness 管理和约束它

每一代解决了上一代的核心问题,同时引入了新的复杂性。这一代的问题是:如何让 AI 可靠地执行

Böckeler 有一段话值得收在这里:

"人类开发者把技能和经验作为一种隐性 harness 带入每个代码库。我们吸收了约定和最佳实践,我们感受过复杂性带来的认知痛苦,我们知道自己的名字会出现在 commit 里。Harness 是把这些东西外显化、明确化的尝试。但它只能走到某一步。"

Harness Engineering 不是要让人类工程师消失。是要让工程师的经验、品味和判断力,以工程化的方式传递给 AI,让 agent 在你的价值观里工作。

能把自己的工程判断力编写成 harness 的人,就是这个新学科的核心建设者。


参考来源

英文一手资料

中文解析与实践


综合整理自 30+ 篇一手资料与开源项目 | 2026-04-13

MCP OAuth 2.0 认证

MCP 鉴权机制详解:基于 OAuth 2.0 的标准实践

前言

MCP(Model Context Protocol)作为连接 AI 助手与外部工具的桥梁,其安全性至关重要。本项目(demo)演示了一套完整的 OAuth 2.0 授权码流程实现,采用标准 Localhost Callback 方案,让 AI 客户端(如 Claude Code)能够安全地访问受保护的 MCP 工具。


1. MCP 鉴权概述

MCP 协议本身支持多种认证机制,其中最标准的方式是借助 OAuth 2.0 授权框架。MCP 鉴权的核心目标是:

  • 身份验证:确认客户端的身份(who are you)
  • 授权控制:决定客户端可以访问哪些工具(what you can do)
  • 会话管理:维护多次请求之间的状态(session)

2. OAuth 2.0 核心概念

2.1 角色定义

角色 说明
Resource Owner 资源所有者,即最终用户
Client 想要访问资源的应用程序(此处为 Claude Code)
Authorization Server 颁发访问令牌的授权服务器
Resource Server 托管受保护资源的服务器(MCP 端点)

2.2 授权码流程(Authorization Code Flow)

┌─────────┐     ┌─────────┐     ┌─────────────┐     ┌───────────┐
│  Client │     │ Browser │     │ Auth Server │     │Token Server│
└────┬────┘     └────┬────┘     └──────┬──────┘     └─────┬─────┘
     │               │                 │                  │
     │ ① 打开授权页  │                 │                  │
     │──────────────▶│                 │                  │
     │               │ ② 显示授权页面  │                  │
     │               │◀────────────────│                  │
     │               │ ③ 用户点击授权  │                  │
     │               │────────────────▶│                  │
     │               │                 │ ④ 生成 code      │
     │               │◀────────────────│  重定向         │
     │ ⑤ code       │                 │                  │
     │◀─────────────│                 │                  │
     │               │                 │                  │
     │ ⑥ 用 code 换 token             │                  │
     │────────────────────────────────▶│                  │
     │               │                 │ ⑦ 返回 token    │
     │◀────────────────────────────────│                  │
     │               │                 │                  │
     │ ⑧ 带 token 访问 MCP             │                  │
     │────────────────────────────────────────────────────▶│

3. 标准 Localhost Callback 方案

本项目采用标准 Localhost Callback 方案,这是 OAuth 2.0 中最安全的公共客户端实现之一。

3.1 方案原理

┌────────────────────────────────────────────────────────────────────┐
│                        Localhost Callback 流程                      │
├────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Client (localhost:3000)                                           │
│        │                                                            │
│        │ ① 启动本地 HTTP 服务器                                      │
│        │                                                            │
│        │ ② 打开浏览器到 Auth Server                                 │
│        ▼                                                            │
│   ┌─────────────┐         Auth Server (localhost:3005)             │
│   │  Browser    │                                                │
│   └──────┬──────┘                                                │
│          │                                                         │
│          │ ③ 用户授权后重定向到                                    │
│          │    localhost:3000/callback?code=xxx                    │
│          ▼                                                         │
│   ┌─────────────┐                                                 │
│   │ Local Server│ ← 同一台机器,同一进程                            │
│   │ 收到 code   │   安全地接收到授权码                              │
│   └─────────────┘                                                 │
│          │                                                         │
│          │ ④ code 通过内存传递(无需网络)                          │
│          ▼                                                         │
│   Client 继续:                                                    │
│          │                                                         │
│          │ ⑤ 用 code 向 /token 换取 access_token                   │
│          │                                                         │
│          │ ⑥ 用 access_token 调用 MCP 端点                          │
│          ▼                                                         │
│   MCP Resource Server                                              │
│                                                                     │
└────────────────────────────────────────────────────────────────────┘

3.2 为什么选择 Localhost Callback?

方案 优点 缺点
Localhost Callback 无额外基础设施,code 在本机传递,安全 仅限桌面客户端
Private URI Scheme 可自定义回调协议 需要系统配置,可能被拦截
Loopback Interface 类似 localhost,跨平台 部分平台可能受限

4. 项目架构

4.1 整体架构图

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Claude Code (MCP Client)                        │
│                                                                             │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐                     │
│   │ OAuth SDK   │    │ MCP Client  │    │  Transport  │                     │
│   │ - register  │    │ - listTools │    │ - HTTP/SSE  │                     │
│   │ - authorize │    │ - callTool  │    │ - Session   │                     │
│   └─────────────┘    └─────────────┘    └─────────────┘                     │
└─────────────────────────────────────────────────────────────────────────────┘
                              │
                              │  OAuth + MCP
                              ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         demo Server (Express)                                │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐ │
│   │                         OAuth Provider                               │ │
│   │   ┌───────────────────┐  ┌───────────────────┐  ┌───────────────┐    │ │
│   │   │ InMemoryClients  │  │   Auth Codes      │  │    Tokens     │    │ │
│   │   │     Store        │  │     Map           │  │     Map       │    │ │
│   │   └───────────────────┘  └───────────────────┘  └───────────────┘    │ │
│   │                                                                          │ │
│   │   端点: /register, /authorize, /token, /.well-known/oauth-*            │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐ │
│   │                    StreamableHTTPServerTransport                      │ │
│   │   • Session 管理  • Bearer Token 验证  • SSE 流式响应                 │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐ │
│   │                         MCP Server (McpServer)                       │ │
│   │   工具: public-info (公共), protected-data (需认证)                   │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

4.2 目录结构

demo/
├── src/
│   ├── server.ts      # OAuth 授权服务器 + MCP 服务器
│   └── client.ts      # OAuth 客户端(MCP 消费者)
├── build/             # 编译输出
├── package.json
├── tsconfig.json
└── .env.example       # 环境变量示例

5. 服务器端实现

5.1 核心组件:DemoOAuthServerProvider

class DemoOAuthServerProvider {
  clientsStore = new InMemoryClientsStore();

  // 授权码存储(10分钟过期)
  private codes = new Map<string, {
    client: OAuthClientInformationFull;
    expiresAt: number;
  }>();

  // 访问令牌存储(1小时过期)
  private tokens = new Map<string, {
    clientId: string;
    scopes: string[];
    expiresAt: number;
  }>();

  // 刷新令牌存储
  private refreshTokens = new Map<string, {
    clientId: string;
    scopes: string[];
  }>();
}

5.2 授权端点(/authorize)

用户访问授权页面,确认后生成授权码并重定向:

app.get('/authorize', (req, res) => {
  const { client_id, redirect_uri, state, scope } = req.query;
  
  // 返回授权确认页面
  res.send(`
    <h1>Authorization Request</h1>
    <form method="POST" action="/authorize/approve">
      <button type="submit">Authorize</button>
    </form>
  `);
});

// 处理授权批准
app.post('/authorize/approve', async (req, res) => {
  await oauthProvider.authorize(client, {
    redirectUri: redirect_uri,
    state
  }, res);  // 重定向到 localhost:3000/callback?code=xxx
});

5.3 Token 端点(/token)

接收授权码,返回访问令牌:

app.post('/token', async (req, res) => {
  const { grant_type, code, client_id, client_secret } = req.body;

  if (grant_type === 'authorization_code') {
    const tokens = await oauthProvider.exchangeCodeForToken(code);
    res.json(tokens);  // { access_token, token_type, expires_in, ... }
  }
});

5.4 MCP 端点(/mcp)—— Bearer 认证

使用 requireBearerAuth 中间件保护 MCP 端点:

app.use('/mcp', 
  requireBearerAuth({
    verifier: oauthProvider,
    requiredScopes: ['mcp:tools']
  }), 
  mcpRouter
);

5.5 工具注册

// 公共工具 - 无需认证
server.registerTool('public-info', {
  description: 'Get public server information (no auth required)'
}, async () => ({ content: [{ type: 'text', text: 'Public info' }] }));

// 受保护工具 - 需要认证
server.registerTool('protected-data', {
  description: 'Get sensitive data (requires Bearer authentication)'
}, async () => ({ content: [{ type: 'text', text: 'Protected data' }] }));

6. 客户端实现

6.1 启动本地回调服务器

async function startCallbackServer(): Promise<{ code: string; server: http.Server }> {
  return new Promise((resolve, reject) => {
    const server = http.createServer((req, res) => {
      const url = new URL(req.url || '/', `http://localhost:${LOCAL_PORT}`);
      
      if (url.pathname === '/callback') {
        const code = url.searchParams.get('code');
        
        if (code) {
          res.end('<h1>Authorization Successful!</h1>');
          resolve({ code, server });
        }
      }
    });
    
    server.listen(LOCAL_PORT);
  });
}

6.2 构建授权 URL

const authUrl = new URL(`${AUTH_SERVER_URL}/authorize`);
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', LOCAL_CALLBACK_URL);  // http://localhost:3000/callback
authUrl.searchParams.set('scope', 'mcp:tools');
authUrl.searchParams.set('response_type', 'code');

await open(authUrl.toString());  // 打开浏览器

6.3 交换 Token

async function getAccessToken(authCode: string): Promise<string> {
  const params = new URLSearchParams({
    grant_type: 'authorization_code',
    code: authCode,
    redirect_uri: LOCAL_CALLBACK_URL,
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET
  });

  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString()
  });

  const tokens = await response.json();
  return tokens.access_token;
}

6.4 创建带认证的 Transport

const transport = new StreamableHTTPClientTransport(SERVER_URL, {
  requestInit: {
    headers: {
      'Authorization': `Bearer ${accessToken}`  // Bearer Token 认证
    }
  }
});

const client = new Client({ name: 'demo-client', version: '1.0.0' }, {});
await client.connect(transport);

// 调用工具
const tools = await client.listTools();
const result = await client.callTool({ name: 'protected-data', arguments: {} });

7. 完整数据流时序图

┌───────────┐     ┌───────────┐     ┌───────────────────────────────────┐
│  Client   │     │  Browser  │     │          Auth Server              │
└─────┬─────┘     └─────┬─────┘     └───────────────┬───────────────────┘
      │                 │                           │
      │ ① 启动本地服务器                             │
      │     localhost:3000                         │
      │                                           │
      │ ② 打开授权页面                              │
      │───────────────────────────────────────────▶ GET /authorize
      │                 │                         │
      │                 │ ③ 显示授权页面           │
      │                 │◀─────────────────────────│
      │                 │                         │
      │                 │ ④ 用户点击 Authorize    │
      │                 │─────────────────────────▶│ POST /authorize/approve
      │                 │                         │
      │                 │ ⑤ 重定向到              │
      │                 │   localhost:3000/callback?code=xxx
      │◀───────────────────────────────────────────│
      │                 │                         │
      │ ⑥ 收到 code    │                         │
      │    (本地进程)    │                         │
      │                 │                         │
      │ ⑦ 用 code 换 token                        │
      │───────────────────────────────────────────▶│ POST /token
      │                 │                         │
      │ ⑧ 收到 access_token                       │
      │◀───────────────────────────────────────────│
      │                 │                         │
      │ ⑨ 带 Bearer Token 调用 MCP               │
      │───────────────────────────────────────────▶│ POST /mcp
      │                 │                         │   Authorization: Bearer xxx
      │                 │                         │
      │ ⑩ 返回受保护数据                          │
      │◀───────────────────────────────────────────│

8. 安全机制

机制 说明
Authorization Code 临时凭证,一次性使用,10分钟过期
Client Secret 客户端身份验证,确保只有合法客户端能获取 token
Bearer Token 每个请求携带,1小时过期
State 参数 防止 CSRF 攻击(可选)
Localhost 回调 code 不经过网络传输,防止拦截
Scope 控制 细粒度权限控制(mcp:tools)

SDD 实战:用 Claude Code + OpenSpec,把 AI 编程变成“流水线”

一、什么是 OpenSpec?

OpenSpec 指的是一个规范驱动开发SDD(Spec-Driven Development) 的范式,为AI编码提供了“规格说明书”,把AICoding从“凭感觉写代码”提升到“按规格任务执行”的高度,告别“开盲盒”式的AI编程。

一句话定义:

在写任何一行代码之前,先定义一份“AI可执行的规格说明书(Spec)”

它的核心思想是:

  • 人类负责:定义规则(Spec)
  • AI 负责:执行规则(生成代码)

(1)什么是规范驱动开发(SDD)?

规范驱动开发(Spec-Driven Development, SDD)是一种软件开发方法论,其核心理念是:

  • 规范定义行为:系统的行为由规范(Specification)明确定义
  • 代码实现规范:代码是对规范的实现,而非规范的替代
  • 规范驱动变更:所有变更都从规范变更开始
  • 规范即文档:规范既是需求文档,也是设计文档

(2)和传统开发有什么不同?

方式 核心输入 AI行为
Prompt 编程 自然语言
OpenSpec 结构化规范 执行

从“描述需求” → “定义系统行为”

二、为什么需要 OpenSpec?

2.1 传统 AI 编程的核心问题

AI 编程助手(如 Claude / Copilot)存在几个致命缺陷:

  • ❌ 模糊输入 → 不稳定输出
  • ❌ 无法形成“系统级约束”
  • ❌ 无版本追踪(改了啥说不清)
  • ❌ 上下文一长就失控
  • ❌ 遗漏重要功能 & 添加了不必要的功能

2.2 OpenSpec 的解决方案

OpenSpec 通过“规范驱动”解决这些问题:

  • ✅ 明确共识:编码前锁定需求
  • ✅ 结构化管理:所有规范集中管理
  • ✅ 可审查:Spec 可读、可评审
  • ✅ 可执行:AI根据确定的需求生成代码
  • ✅ 可追踪:所有变更都有历史

❗ 其本质就是: AI 不再自由发挥,而是严格执行 Spec

三、快速开始 OpenSpec

3.1 环境准备

Node.js >= 20.19.0

全局安装

npm install -g @fission-ai/openspec@latest

验证是否安装成功:

openspec --version

image.png

3.2 初始化项目

cd openspec-demo
openspec init

image.png

初始化过程中会让你选择 AI 编程工具(推荐 Claude Code)。

image.png 完成后会生成核心目录:

image.png

openspec/
├── specs/        # 当前系统规范(源真相)
├── changes/      # 变更提案
└──── archive/      # 历史归档

四、OpenSpec 核心能力(Skills)

4.1 openspec-propose(发起变更)

  • 核心作用:发起一个变更提案

  • 做什么:

    • openspec/changes/ 下创建一个独立变更目录
    • 引导你编写变更说明(proposal.md) :为什么改、改什么、影响范围
    • 生成待完善的规格文档(spec),先和AI对齐需求,在写代码
  • 场景:

    • 新增功能
    • 重构模块
    • 修复重大问题前的需求对齐

4.2 openspec-explore(分析系统)

  • 核心作用:探索与分析当前规范与变更

  • 做什么:

    • 读取 openspec/specs/ 里的现有规范,帮你理解系统当前行为
    • 分析待处理的变更提案,评估影响范围、依赖关系
    • 辅助你细化方案、拆分任务,避免开发时偏离规范
  • 适用场景:

    • 开发前做技术调研、
    • 理解现有系统、
    • 评估变更风险

4.3 openspec-apply-change(生成代码)

  • 核心作用:将规范变更落地到代码实现

  • 做什么:

    • 读取指定变更提案的规范文档
    • 引导 Claude 按规范生成 / 修改代码,严格对齐 spec
    • 确保代码实现与规范完全一致,避免 “写的和想的不一样”
  • 适用场景:

    • 规范定稿后
    • 正式开发 / 迭代代码阶段

4.4 openspec-archive-change(归档)

  • 核心作用:归档已完成的变更,更新项目规范

  • 做什么:

    • 将已实现的变更规范合并到 openspec/specs/ (项目 “源真相”)
    • 把变更目录移动到 openspec/changes/archive/ 归档
    • 生成交付记录,让项目规范始终保持最新状态
  • 适用场景:代码开发完成、测试通过后,正式纳入项目规范

4.5 整体工作流程

  1. propose → 定义需求,定义规范
  2. explore → 分析影响
  3. apply-change → 按照规范生成代码
  4. archive → 更新规范,归档

这其实就是:把软件开发变成“规范驱动流水线”

五、实战:用 OpenSpec + Claude Code 对TodoList进行优化

这是上一篇文章使用Claude Code实现的TodoList

Claude Code 入门实战:从安装配置到真实项目落地

image.png

本次需求:

  1. 待办事项的列表改为使用Table展示,并且支持批量改变完成状态和删除功能;
  2. 在列表中增加创建时间和更新时间两个字段,展示格式为YYYY-MM-DD hh:mm:ss
  3. 现状:添加相同的待办事项可以添加成功;期望:不允许添加重复的待办事项,并给出存在重复的待办事项提示;
  4. 改为Table展示后再调整下页面的样式,待办事项清单的宽度以及背景颜色;

Step 1:通过 /openspec-propose调用openspec的skills

  • /openspec-propose提交需求后,系统自动在openspec/change目录下创建了本次需求的独立目录enhance-todo-list,这里的目录名称可以理解为就是本次的需求ID
  • 目录自动生成标准化需求文档,支持反复评审打磨,确保需求清晰,边界明确后再进入开发阶段,避免需求存在偏差

image.png

创建proposal.md提案文件

## Why

当前待办事项应用使用List组件展示,功能较为基础,不支持批量操作。同时,缺少对重复事项的校验机制,以及用户无法直观查看待办事项的创建和更新时间。这些限制降低了应用的用户体验和管理效率。

## What Changes

- **UI组件升级**: 将List组件替换为Table组件,支持更丰富的展示和交互
- **批量操作**: 新增批量改变完成状态和批量删除功能
- **时间字段增强**: 添加创建时间(createdAt)和更新时间(updatedAt)字段,格式化为 `YYYY-MM-DD hh:mm:ss`
- **重复校验**: 添加待办事项内容去重机制,防止重复添加

## Capabilities

### New Capabilities
- `batch-todo-operations`: 批量操作待办事项(批量完成/取消完成、批量删除)
- `todo-time-tracking`: 待办事项时间记录和展示(创建时间、更新时间)
- `todo-duplicate-validation`: 待办事项重复性校验

### Modified Capabilities
- `todo-crud`: 基础待办事项增删改查(添加重复校验到创建操作)

## Impact

- **代码变更**:
  - `src/components/TodoList.tsx`: 重构为Table组件,添加批量操作逻辑
  - `src/types/todo.ts`: 添加updatedAt字段
  - 新增依赖: `dayjs` 时间处理库

- **API变更**:
  - addTodo: 添加重复校验逻辑
  - 新增: batchToggleTodos、batchDeleteTodos 方法

- **用户体验**:
  - 提供更高效的批量操作能力
  - 更清晰的时间信息展示
  - 避免重复待办事项的创建

  • openspec/change/enhance-todo-list/specs 这个文件里面的内容可以理解为是本次需求的测试用例文件。
  • design.md & tasks.md是根据需求创建的设计文档和将需求拆解为一个个的Task文档
  • 这里就需要我们去确认这个task.md文档中拆解的task是否合理,是否可以满足我们的需求,在后续apply的时候会去执行文档中所有的task

image.png

Step 2: 自动化生成代码,上述文档确认完成后,执行指令: /openspec-apply-change 需求ID

系统将会自动按照tasks.md中的任务清单,逐个执行任务

image.png

完成需求后页面效果: image.png

查看实现的代码,整个过程中几乎没有“手写业务代码”,而是把精力放在“定义系统行为”上面。

这就是OpenSpec传统 AI 编程最大的不同。 image.png

Step 3: 执行 /openspec-archive-change 需求ID将本次的迭代的需求进行归档操作,方便后续追溯

  • 执行完本次的需求文件夹会被移动到openspec/changes/archive/日期+需求ID目录下

image.png

六、总结

OpenSpec 的意义,不只是一个工具,更像是一种开发范式的转变:

从“人写代码,AI辅助”
到“人定义系统,AI负责实现”

在这种模式下:

  • 代码不再是“源真相”,规范才是
  • AI 不再是“猜需求”,而是“执行规则”
  • 开发过程从“不断试错”,变成“按规范推进”

这背后,其实是软件工程的一次“回归”:

👉 回归到“用明确的约束定义系统行为”

当 AI 编码能力越来越强,真正拉开差距的,不再是“谁写代码更快”,而是:

谁能定义出更清晰、更严谨的系统规范

⚡精通Claude第3课:学会用Skills让Claude变身为专属专家

skill.png

Skills 是 Claude Code 的大招——你可以给它装上各种"技能包",让它变成代码review专家、部署达人、或者任何你需要的专业助手。一次配置,随时调用。

image.png

你有没有遇到过这种情况?

  • 每次让 Claude 帮你review代码,你都得先把评审标准说一遍。
  • 每次让它部署,你都得解释一遍流程。
  • 每次让它写文档,你都得强调一遍格式要求。

累不累.jpg

Claude Agent Skills 就是来解决这个问题的。它像是给 Claude 装了一个"技能插槽",你提前把专业知识塞进去,以后 Claude 一看到相关任务,自动就调用对应的技能——不用你重复唠叨。


什么是 Skills?说人话!

Skills = 预置的专业知识包。

你可以把它理解成:

  • 医学院的选修课:Claude 原本是个通才,你给它上一门"代码review专业课",它就成了这方面的专家
  • 厨房的调料盒:提前准备好各种调料,做菜时直接撒,不用每次都现调
  • 武侠小说的武功秘籍:把内功心法提前输入,Claude 遇到敌人自动出招

Skills 最大的特点:用的时候才加载,不用时不占地方

这就厉害了——你可以装几十个技能,Claude 也只会在真正用到的时候才把对应技能的内容调进来。不会把上下文窗口塞爆。


三层加载机制(渐进式披露)

Skills 用了一种很聪明的设计,分三层加载:

┌─────────────────────────────────────────────┐
│  第1层:元数据(约100 tokens)               │
│  - 技能名称 + 简短描述                        │
│  - Claude 启动时就知道了这些技能存在           │
└──────────────────────┬──────────────────────┘
                       │ 触发技能时
┌──────────────────────▼──────────────────────┐
│  第2层:指令(约5k tokens)                   │
│  - SKILL.md 的正文内容                        │
│  - 工作流程、指导原则                         │
└──────────────────────┬──────────────────────┘
                       │ 需要更多资源时
┌──────────────────────▼──────────────────────┐
│  第3层:资源文件(无限)                      │
│  - 模板、脚本、示例代码                        │
│  - 按需加载,不进上下文                        │
└─────────────────────────────────────────────┘

用人话讲就是:

  1. 启动时:Claude 知道你有 N 个技能,每个技能是干嘛的
  2. 触发时:Claude 发现这个任务需要某技能,才把技能说明书加载进来
  3. 需要时:Claude 发现还需要模板或脚本,才去读取对应文件

这样设计的好处:你装 100 个技能也不会变慢,因为 Claude 不是一次性全加载。


Skill 的目录结构

一个技能长这样:

my-awesome-skill/
├── SKILL.md              # 主角,必须有
├── templates/            # 模板文件夹
│   └── output-format.md
├── examples/             # 示例文件夹
│   └── sample-output.md
└── scripts/              # 脚本文件夹
    └── validate.sh

最核心的是 SKILL.md,长这样:

---
name: my-skill
description: 这个技能是干嘛的,什么时候该用它
---

# 技能标题

## 使用说明
一步一步告诉 Claude 该怎么做

## 注意事项
有哪些坑要避开

举个例子:做一个代码review专家

假设你想让 Claude 每次review代码都按照你公司的标准来:

目录结构:

~/.claude/skills/code-review/
├── SKILL.md
├── templates/
│   └── review-checklist.md
└── scripts/
    └── analyze-metrics.py

SKILL.md 写起来:

---
name: code-review-expert
description: 代码评审专家,专注安全、性能、质量分析。
              当你提到 code review、代码评审、PR review 时触发。
---

# 代码评审专家

## 评审维度

1. **安全**:认证授权、数据泄露、注入风险
2. **性能**:算法效率、数据库查询优化
3. **质量**:SOLID原则、命名规范、测试覆盖
4. **可维护性**:代码可读性、函数长度、圈复杂度

## 评审流程

1. 先通读代码,理解整体结构
2. 按上面4个维度逐一检查
3. 整理问题,按严重程度排序
4. 给出具体的修复建议

## 严重程度定义

- **Critical**:必须立即修复,有安全风险
- **High**:应该在下次迭代前修复
- **Medium**:建议修复,不紧急
- **Low**:可选的优化项

详细 Checklist  [templates/review-checklist.md](templates/review-checklist.md)

review-checklist.md 长这样:

# Code Review Checklist

## 安全性
- [ ] 是否有 SQL 注入风险
- [ ] 用户输入是否做了校验
- [ ] 敏感数据是否明文存储
- [ ] 权限校验是否完整

## 性能
- [ ] 是否有 N+1 查询问题
- [ ] 循环中是否有不必要的数据库调用
- [ ] 是否需要加缓存
- [ ] 大数据量是否有分页

## ...

现在,当你跟 Claude 说"帮我review一下这个PR",它自动就知道:

  • 要从哪几个维度评审
  • 问题怎么分类
  • 严重程度怎么定义
  • 该输出什么格式的报告

不需要你每次都解释一遍。


控制 Claude 什么时候能调用技能

Skills 有三种调用模式,通过 frontmatter 控制:

---
# 模式1:默认(你也可以调用,Claude 也可以调用)
# 不写任何额外配置就行

# 模式2:只有你能调用,Claude 不能主动用
disable-model-invocation: true
# 适合有副作用的操作,比如部署、删除数据

# 模式3:只有 Claude 能调用,你看不到(不显示在 / 菜单)
user-invocable: false
# 适合后台知识,比如解释旧系统怎么工作的

用人话讲:

  • disable-model-invocation: true = 这个技能太危险了,只有我能触发

禁止模型自动调用该技能,仅允许用户手动通过 /skill-name 触发

  • user-invocable: false = 这是 Claude 的私藏知识,不需要当命令用

用户手动不能调用,只能模型自动调用


动态内容注入

Skills 支持用反引号执行命令,把结果塞进技能内容里:

---
name: pr-summary
description: 总结 Pull Request 的内容
---

## PR 信息
- PR 差异:!`gh pr diff`
- PR 评论:!`gh pr view --comments`
- 改动的文件:!`gh pr diff --name-only`

## 你的任务
根据以上信息,生成一份 PR 总结

image.png 比如这里有一个自动生成commit信息的skill

  1. git status - 显示当前工作区的状态(有哪些文件被修改、暂存或未跟踪)
  2. git diff HEAD - 显示 HEAD 指向的提交与工作区之间的差异(已修改但未暂存的内容)
  3. git branch --show-current - 显示当前所在的分支名称
  4. git log --oneline -10 - 显示最近 10 条提交记录,每条只显示一行(哈希值前7位 + 提交信息)

这些命令组合在一起,能快速了解仓库的完整状态:当前分支、有哪些变更、以及近期提交历史。这通常用于提交前的检查或生成变更记录。

!command`` 会在技能内容加载前执行命令,输出结果直接拼进去。Claude 拿到的时候已经是展开后的完整上下文了。


用 subagent 运行技能(隔离执行)

有时候技能执行起来很复杂,你会想把它放到一个独立的子进程里跑,不占用主会话的上下文。

---
name: deep-research
description: 深入研究某个主题
context: fork        # 关键:fork 一个独立子agent
agent: Explore      # 用 Explore 类型
---

深入研究 $ARGUMENTS:
1.  Glob  Grep 找相关文件
2. 读代码,分析逻辑
3. 总结发现,附上具体文件引用
  • context: fork:创建隔离的子对话 / 子 Agent,不污染主上下文

  • agent: Explore:使用探索专用 AI,擅长遍历、搜索、分析项目结构

context: fork 会在一个独立子 agent 里执行这个技能,子 agent 有自己的上下文窗口。适合:

  • 研究任务,需要深度探索
  • 复杂任务,步骤很多
  • 你不想让主会话变乱的时候

技能放在哪?四种作用域

类型 位置 谁能用 场景
企业级 管理员配置 全公司 公司统一规范
个人 ~/.claude/skills/ 只有你 个人工作流
项目 .claude/skills/ 项目成员 团队标准
插件 插件目录 看插件配置 插件附带

团队协作推荐用项目级技能:丢进 .claude/skills/ 目录,commit 到 git,团队成员 pull 下来就能用。


实际使用场景

场景1:每次代码提交都要规范信息

name: commit-helper
description: 帮助写规范的 commit message
---

# Commit Message 助手

## 格式要求

():

[optional body]

[optional footer]


## Type 只能选这些
- feat:新功能
- fix:bug修复
- docs:文档改动
- style:格式(不影响代码)
- refactor:重构
- test:测试
- chore:构建/工具

## 示例
feat(auth): 添加微信登录支持

实现了微信 OAuth2.0 登录流程
- 扫码登录
- token 刷新
- 退出登录

## 你的任务
根据我给的改动,写出符合格式的 commit message

场景2:部署要按流程来,不能出错

name: deploy
description: 部署应用到生产环境
disable-model-invocation: true  # 太危险,Claude 不能自己触发,只能/name,手动触发
---

部署 $ARGUMENTS 到生产环境:

1. 运行测试:`npm test`
2. 构建应用:`npm run build`
3. 推送部署目标
4. 验证部署是否成功
5. 报告部署状态

如果任何步骤失败,立即停止并报告错误。

场景3:品牌调性知识(Claude 自己看)

name: brand-voice
description: 确保输出内容符合品牌调性(Claude 后台使用)
user-invocable: false
---

## 语气要求
- 友好但不随意
- 清晰简洁,不用黑话
- 自信但不傲慢
- 有同理心,理解用户需求

## 写作规范
- 用"你"称呼读者
- 用主动语态
- 句子控制在20字以内
- 先说价值,再说细节

最佳实践

1. 描述要具体,包含触发词

# ❌ 太泛
description: 帮助处理文档

# ✅ 具体,Claude 知道什么时候该用
description: 提取 PDF 中的文字和表格,填写表单,合并文档。
             当你提到 PDF、表单、文档提取时触发。

2. 一个技能做一件事

# ❌ 太宽泛
name: document-helper
description: 处理各种文档相关任务

# ✅ 专注一件事
name: pdf-extractor
description:  PDF 文件提取文字、表格、图片

3. SKILL.md 控制在 500 行以内

详细的检查清单、API 文档放到 templates/references/ 目录,Claude 需要时再加载。

4. 描述要写清楚"什么时候用"

这是 Claude 决定是否触发技能的关键依据。


常见问题

Q: Claude 不触发我的技能怎么办? A: 检查 description 是否包含了用户会说的关键词。描述越具体,触发越准确。

Q: 技能触发太频繁怎么办? A: 把 description 写窄一点,或者加上 disable-model-invocation: true

Q: 安装太多技能会变慢吗? A: 不会。Skills 是渐进式加载,Claude 只在触发时才会加载对应技能的内容。

Q: 技能冲突怎么办? A: 优先级:企业 > 个人 > 项目。同名技能,高优先级生效。


总结

Skills 是什么?提前预置的专业知识包。

为什么有用?不用重复唠叨,Claude 自动调用。

怎么用?

  1. 创建 .claude/skills/<name>/SKILL.md
  2. 写清楚 name 和 description
  3. 描述里加上触发关键词

现在去试试吧,给你常用的工作流建一个技能,你会发现 Claude 突然变得专业多了。


更多资料

连载05-Claude Skill 不是抄模板:真正管用的 Skill,都是从实战里提炼出来的

别再直接 Fork 别人的 Claude Skill:真正有用的 Skill,都是从项目里长出来的

AI Coding 系列第 05 篇 · 核心工具

我第一次批量导入公开 Skill 模板的时候,是真的以为自己走了捷径。

GitHub 上一堆 star 很高的仓库,code review、需求分析、文档编写、调研、拆任务,看起来什么都有。我当时的想法很直接:既然别人已经把常见工作流整理好了,我直接 fork 一份,全量导入,不就能让 Claude 立刻更稳、更懂项目吗?

结果用了几天,我反而越来越不放心。

不是因为它“明显做错了什么”,而是因为它总在看起来没问题的地方出问题。格式完整,措辞专业,检查项也不少,可真正让我在项目里反复吃亏的那几件事,它一次都没替我盯住。异步链里是不是又漏了 await,这次 migration 有没有回滚方案,新同学是不是又顺手写了 throw new Error(),数据库 schema 改了之后 Prisma 类型是不是也一起更新了。

它会提醒一堆“大家普遍都应该注意”的东西,却不知道“我们团队到底最怕什么”。

后来我才明白,问题不是 Skill 机制不好,而是我导入的根本不是“自己的 Skill”,只是别人整理好的经验。

这些经验当然有价值,但它们解决的是共性问题,不会天然长成你项目里的“肌肉记忆”。

真正有用的 Skill,恰恰应该做一件事:

把那些你本来总要重复提醒、总会漏掉、总会在项目里反复踩坑的动作,固化成默认动作。

也就是一句话:

Skill 的本质,不是收藏经验,而是固化默认动作。

这篇文章会从最基本的边界讲起,一路走到 SKILL.md、源码机制、任务类型和可执行能力。内容不少,但我尽量只保留真正有助于你在项目里把 Skill 用起来的部分。


NotebookLM Mind Map.png

先说结论

如果你只记住这几条,这篇文章就已经值回时间:

  • 对所有任务都生效的规则,写进 CLAUDE.md;只对某类重复任务生效的,做成 Skill;只对这一次有效的,写进 Prompt
  • 只有“输入相对稳定、输出有模式、而且容易漏步骤”的任务,才值得沉淀为 Skill
  • 通用 Skill 模板只能当原材料,项目级 Skill 必须自己裁剪、自己维护
  • description 不是装饰字段,它承担了触发场景的职责,最好把 Use when... 直接写进去,关键词前置、长度克制
  • Claude 启动时主要只看 frontmatter,Skill 正文在真正触发时才按需载入
  • allowed-tools 是权限边界,不是行为建议;paths 是条件激活,不是说明文字
  • 第一个 Skill 不要挑最关键的任务,先拿中等风险任务练手

一、公开 Skill 模板为什么一开始很香,后来却越用越别扭

我现在反而会对“看起来很全”的公开 Skill 模板保持一点警惕。

不是因为它们没用,而是因为它们太容易制造一种错觉:好像什么都覆盖到了,但真正最重要的东西其实没进去。

公开模板最常见的问题,不是方向错,而是下面这三种。

1. 太宽泛

它什么都管一点,但什么都不够深。

它会告诉你“注意异常处理”“注意性能”“注意安全”,这些当然没错。但这些话本身不构成你项目里的工作流。它不知道你们统一用的是 AppError,不知道你们数据库变更必须检查回滚,也不知道你们哪几个目录历史包袱最重。

2. 太嘈杂

50 行模板里,真正有价值的可能只有 5 行。

剩下的 45 行不是完全没用,而是在和那 5 行争夺 Claude 的注意力。对于 agent 来说,规则不是越多越强。很多时候,8 行写透项目约束的 Skill,比 50 行“样样都提一点”的模板更有用。

3. 太不像你的项目

这点最致命。

公开模板知道“大家普遍应该注意什么”,但不知道“你们团队反复死在哪些地方”。而真正有价值的 Skill,恰恰应该把那些项目特有、团队高频踩坑的东西固化下来。

说得更直白一点:你把一个新同事扔进团队,给他一份行业通用培训材料,当然比什么都不给强;但如果你不告诉他“我们团队最容易出错的是哪三件事”,他依然干不好你最在意的活。

所以正确姿势不是“找一个最全的模板直接用”,而是:

先借鉴,再裁剪,最后只留下真正属于你项目的那几条。

公开模板到项目 Skill 的提炼路径


二、先把 Prompt、CLAUDE.md、Skill 这三件事彻底分清楚

很多人不是不会写 Skill,而是一开始就把这三件事混在一起了。

判断方法其实很简单,只问一个问题:

这个要求的作用范围到底有多大?

  • 这个要求对所有任务都成立吗?如果是,放 CLAUDE.md
  • 这个要求只对某一类任务成立吗?如果是,做成 Skill
  • 这个要求只对这一次成立吗?如果是,写进 Prompt

举几个特别典型的例子:

“所有 throw 必须是 AppError
这是全局规则。不管你是在写新功能、修 bug,还是做重构,都要遵守。它应该进 CLAUDE.md

“代码审查时按固定顺序检查数据库、异步和错误处理”
这只在 code review 这种任务里才触发,它不是全局规则,而是任务模板,所以应该做成 Skill

“这次先只分析原因,不要动代码”
这只对当前这次任务有效,应该写进 Prompt

最容易搞混的是 CLAUDE.mdSkill。它们都能约束 Claude 的行为,但本质完全不同:

  • CLAUDE.md 是永远生效的规则
  • Skill 是遇到对应任务才触发的模板

如果要打个比方:

  • CLAUDE.md 是交通规则
  • Skill 是导航路线
  • Prompt 是你这次上车前临时交代的一句话

这三层一旦分清楚,后面 80% 的混乱都会自动消失。

Prompt、CLAUDE.md、Skill 的边界图

一个常见误判:很多问题根本不需要写 Skill

我后来发现,很多人想写 Skill,并不是因为真的存在一个稳定、重复、值得沉淀的任务,而是因为这一次和 Claude 协作得不顺

比如目标没说清,边界没收紧,上下文没给够,或者你真正缺的是一条全局规则,却误以为自己需要一份任务模板。这个时候你如果急着把它沉淀成 Skill,本质上只是把一次性的混乱模板化。

几个很常见的误判场景是:

  • 这次需求本身还在摇摆,连你自己都没想清楚要什么
  • 这个问题只发生过一次,下次未必还会以同样的形状出现
  • 你真正缺的是全局约定,比如错误处理、目录规范、命名规则
  • 你只是想表达“这次先别改代码”“这次先只分析原因”这种一次性约束

写 Skill 之前,先问自己一句话:

这个问题下次还会以差不多的形状再来一次吗?如果不会,先别急着写 Skill。


三、什么时候一个任务真的值得被沉淀成 Skill

不是所有重复任务都值得沉淀。

我现在给自己的标准其实很克制,就一句话:

同一类任务做了三次以上,而且每次都要重新给 Claude 解释背景。

反过来说,如果某个任务每次背景和目的都完全不同,就不值得沉淀。比如“写文档”这个动作本身很常见,但公司文档、API 文档、用户手册的写法完全不同,它们应该是三个不同的 Skill,而不是一个叫“写文档”的通用模板。

在真正开始写之前,我会先做三个检查。

1. 输入是否稳定

“根据 Figma 设计稿生成 React 组件”这种任务,输入格式相对稳定,比较适合沉淀。

“根据 SQL 查询结果生成图表”这种任务,每次数据格式和图表类型都可能差很多,Skill 会很难写得稳。

2. 输出是否有共同模式

“写 Pull Request 描述”很适合,因为它天然就有固定框架:改了什么、为什么改、怎么测试。

但“和 AI 讨论技术方案”这种任务,每次深度、重点、结论都不同,就不太适合硬沉淀成一个模板。

3. 有没有容易漏掉的关键步骤

最值得沉淀成 Skill 的任务,通常不是“最复杂”的任务,而是那些不特别提醒就容易漏一步的任务。

Skill 最有价值的地方,不是让 Claude 变得更聪明,而是把你每次最容易忘的检查项,固化成默认动作。

所以一个任务如果同时满足下面三点:

  • 输入相对稳定
  • 输出有共同模式
  • 总有一两步容易漏

它就很值得沉淀成 Skill。

什么任务值得沉淀成 Skill


四、从一个真实痛点开始,走完 Skill 的提炼过程

光讲判断标准还是有点抽象,不如走一遍完整例子。

代码审查,几乎每个后端工程师每周都在做,也是最容易进入“重复解释”困境的任务。用它来走一遍完整的 Skill 提炼过程会很清楚。

你反复踩的坑

假设你们团队每周都做代码审查,而且总在重复盯这几件事:

  • 有人改一个功能,顺手动了三个不相关模块
  • 新同学不知道项目里统一用 AppError,直接 throw new Error()
  • Promise 链里漏了 await
  • 数据库查询没有索引,或者潜在 N+1 没被看出来

这就是非常典型的“该沉淀 Skill 的信号”。

先设计内容,再去想格式

一个好 Skill,先别急着写文件。先把内容层想清楚,只要回答四个问题:

1. 什么时候用

不是写“代码审查”四个字,而是写清楚触发场景。

❌ 代码审查
✅ 当我提交 PR 前,检查我的 TypeScript 后端代码是否符合项目约定

差别在于:模糊的描述会让 Claude 在不该用的时候乱触发,而具体的场景描述更容易精准命中。

2. 按什么顺序做

步骤尽量不要超过五步。

你从公开模板里借灵感,但通用模板有 50 行,而你真正关心的可能只有四件事:改动范围、错误处理、异步操作、数据库查询。

1. 读完整个改动的 diff,确认改动是否只涉及这个 PR 的范围
2. 检查错误处理:所有 throw 都必须是 throw new AppError()
3. 检查异步操作:Promise 链是否有遗漏的 await
4. 检查数据库查询:是否有 N+1 问题,关键查询是否 explain 过

3. 输出长什么样

不要写“请清晰输出”。这种话几乎没有约束力。直接给格式。

🔴 Critical: ...
🟡 Warning: ...
✅ Suggestion: ...
Summary: X critical issues to fix before merge.

4. 什么时候不适用

写清楚边界比写清楚功能更重要。

比如:

  • 不审查 UI 层代码
  • 不关注代码风格
  • 改动超过 500 行先拆 PR

这些“我不做什么”的声明,往往比“我会做什么”更能防止 Claude 越界。

到这里,你脑子里其实已经有一个能用的 Skill 了。下一步只是把它放进 Claude Code 认识的格式里。


五、真正落到 SKILL.md 文件层,哪些字段值得你认真写

一个完整的 SKILL.md,通常会长这样:

---
name: code-review
description: Review TypeScript backend code before merging. Use when asked to review code, check a PR, or verify implementation before committing.
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash(git diff *)
argument-hint: "[PR 分支名或文件路径]"
arguments:
  - target
---

# Code Review

## 步骤
1. 读取 ${target} 的改动 diff
2. 检查错误处理:所有 throw 必须是 throw new AppError()
3. 检查异步操作:Promise 链是否有遗漏的 await
4. 检查数据库查询:是否有 N+1 问题

## 输出格式
🔴 Critical: ...
🟡 Warning: ...
✅ 通过: ...

这里最值得你认真写的,其实是下面几个字段。

name
名字别太抽象。要让人一眼知道它是做什么的。像 helperutilstools 这种名字几乎没有路由价值,远不如 code-reviewpr-summaryapi-conventions 这种具体命名。

description
这是现在最关键的字段。它不只是“简介”,还承担了“触发场景”的职责。你最好直接把 Use when... 写进去,而不是写一句空话。更重要的是,别把它写成长段说明文。关键词尽量前置,长度最好控制在 250 字符左右,太长往往只会稀释命中信号。官方还特别提醒,description 最好用第三人称去写,像 “Analyzes pull requests...” 这种句式,比 “I can help...” 或 “You can use this...” 更稳。

allowed-tools
它决定这个 Skill 具备哪些能力。这个字段后面我会在源码部分展开讲,因为它比很多人想象的更“硬”。

arguments
让 Skill 接受参数,比如目标文件、目录、分支名。${target} 会在正文里被替换成你传进去的实际值;如果你喜欢按位置拿参数,也可以用 $0$1 这类方式。

还有几个很好用,但不是每次都要上的字段。

argument-hint
告诉调用者这个 Skill 期待什么参数。

model: haiku
简单任务可以指定更轻量的模型,直接省成本。像格式化、重命名、简单改写这类工作,很多时候没必要上更重的模型。

paths
让 Skill 只在某些路径下激活。适合模块边界明确的项目。

context: fork
高风险操作放进独立上下文,避免污染主会话。

disable-model-invocation: true
禁止 Claude 自动触发,只允许你手动 /skill-name 调用。部署、发版、发邮件这类有副作用的 Skill,应该优先考虑加上。

大多数 Skill 根本不需要把字段填满。真正实用的思路不是“功能全”,而是“正好够用”。

如果一个 Skill 只是做常规代码审查,namedescriptionallowed-toolsarguments 往往就够了。只有当你真的遇到参数化、模块隔离、上下文隔离这些需求时,再往上加。

SKILL.md 不是整个 Skill,它只是入口

很多人以为一个 Skill 就是一份 SKILL.md。其实不是。

更实用的做法通常是:把 SKILL.md 控制在足够短、足够清楚的范围里,让它承担“入口”和“调度”职责;真正长的规范、示例、脚本都拆出去。

一个 Skill 目录完全可以长这样:

my-skill/
├── SKILL.md
├── reference.md
├── examples/
│   └── sample.md
└── scripts/
    └── helper.py

这里的关键点不是“可以放很多文件”,而是:这些文件不会自动加载,必须在 SKILL.md 里显式引用。

比如:

## 参考资料
- 完整的 API 规范见 [reference.md](reference.md),需要查接口细节时读它
- 期望的输出格式见 [examples/sample.md](examples/sample.md)

这个设计和前面说的懒加载是同一套思路:不是 Skill 触发时把所有材料都灌进上下文,而是只在真正需要的时候再去读。

所以:

  • reference.md 适合放项目特有知识,比如内部 API 规范、禁用库、架构约定
  • examples/ 适合放期望输出样例,帮助 Claude 对齐格式
  • scripts/ 适合放真正可执行的辅助脚本,让 Skill 不只是“描述怎么做”,还能“先把上下文准备好”

这点很重要,因为它决定了 Skill 的上限不是“几行 prompt”,而是“一个有入口、有知识、有执行能力的局部工作流”。

Skill 放在哪,决定它是谁的能力

这点很容易被忽略,但工程上很重要。

同样是一个 Skill,放在不同位置,意义完全不一样:

  • ~/.claude/skills:你个人所有项目都能用,适合个人长期习惯
  • .claude/skills:只在当前项目生效,适合团队项目约定
  • <plugin>/skills:跟着插件走,适合做模块化分发

如果不同层级里恰好有同名 Skill,优先级也不是平均的。官方规则更接近:企业级配置优先于个人级,个人级优先于项目级;插件 Skill 因为带命名空间,通常不会和前面这些直接撞名。

官方文档里甚至把这件事讲得很直接:Skill 存放的位置,本身就是它的作用域设计。

这背后的工程含义非常大。

如果你把一个强项目耦合的 Skill 放进个人目录,它就会带着这个项目的假设跑到别的仓库里;反过来,如果你把一个本该跨项目复用的通用 Skill 只塞在项目目录里,它的复用价值又被锁死了。

在 monorepo 里,这件事更有意思。Claude Code 会自动发现子目录下的 .claude/skills/。也就是说,你完全可以让 packages/frontend/.claude/skills/ 只服务前端包,让 packages/backend/.claude/skills/ 只服务后端包,而不是把所有知识都堆在仓库根目录。

这时 Skill 就不只是“提示词文件”,而是团队知识的分发机制:

  • 个人层的 Skill,固化的是你的工作习惯
  • 项目层的 Skill,固化的是团队约定
  • 包级 Skill,固化的是模块边界里的局部知识

如果你能把这层想清楚,很多“这个规则到底该放哪”的问题,答案会比只看内容本身更清楚。


六、如果只停在经验层,这篇还差半口气:我后来去翻了源码

前面这些判断,靠经验其实也能总结出来。

但我后来还是不太满足。因为有几个问题如果不看实现,心里总会悬着:

  • description 到底是不是自动触发的关键?
  • allowed-tools 到底只是提示,还是硬限制?
  • paths 到底是真过滤,还是只是写给人看的说明?

我后来去翻了一遍源码,结论是:这些字段比我一开始以为的更“硬”。

1. 为什么触发逻辑主要看 frontmatter,而不是正文

loadSkillsDir.ts 里有一个函数 estimateSkillFrontmatterTokens,注释写得非常直接:

/**
 * Estimates token count for a skill based on frontmatter only
 * (name, description, whenToUse) since full content is only loaded on invocation.
 */
export function estimateSkillFrontmatterTokens(skill: Command): number {
  const frontmatterText = [skill.name, skill.description, skill.whenToUse]
    .filter(Boolean)
    .join(' ')
  return roughTokenCountEstimation(frontmatterText)
}

这段代码背后的意思非常重要。

Claude Code 启动时,主要只把每个 Skill 的 frontmatter 信息算进上下文。Skill 正文不是一开始就全量塞进去,而是在你真正触发它的时候才加载。

这直接解释了两件事。

第一,Claude 不是先把你整篇 Skill 读完再判断要不要触发,它先看的就是前面这几行。换句话说,触发效果主要取决于 frontmatter,不取决于正文写得多漂亮。

第二,Skill 多不等于上下文立刻爆炸,因为启动时压进去的不是全文,而是 frontmatter。

源码里保留了 whenToUse 这个概念,但从现在的文档实践看,推荐做法已经更偏向把触发描述直接写进 description。所以对大多数人来说,最稳的策略不是纠结“要不要额外写一个触发字段”,而是把 description 写得具体、可命中、带触发场景。

比如:

description: Review TypeScript backend code before merging. Use when asked to review code, check a PR, or verify implementation before committing.

比“代码审查 Skill”这种描述强太多了。

源码注释里其实还暗含了一个很实用的提醒:触发描述不是越长越稳。冗长的 whenToUsedescription 不会线性提高命中率,很多时候只是在白白消耗首轮缓存和注意力。所以对这个字段最好的优化,不是“多写一点”,而是“把真正会命中的词放到前面”。

2. 为什么 allowed-tools 不是建议,而是权限边界

这一点是我看源码之后感受最强的一处。

Skill 执行时,getPromptForCommand 会在返回内容之前把 allowedTools 写进工具权限上下文:

getAppState() {
  const appState = toolUseContext.getAppState()
  return {
    ...appState,
    toolPermissionContext: {
      ...appState.toolPermissionContext,
      alwaysAllowRules: {
        ...appState.toolPermissionContext.alwaysAllowRules,
        command: allowedTools,
      },
    },
  }
}

这说明 allowed-tools 不是“提醒 Claude 尽量这样做”,而是权限层的强制限制。

比如一个 code review Skill 只开放 ReadGrepGlobBash(git diff *),那它就不是“理论上不该写文件”,而是从架构上根本没有写文件的能力Bash(git diff *) 这种写法也不是装饰,它真的只允许 git diff 开头的命令,其他 Bash 调用会被挡住。

这让我对 allowed-tools 的理解完全变了。它不是“不信任模型”,而是最小权限设计。就像你给数据库只读账号只开 SELECT 权限,不是因为你怀疑这账号会作恶,而是因为这个任务本来就不该拥有写权限。

3. 为什么 paths 不是说明文字,而是条件激活机制

源码里,带 paths 的 Skill 在加载时会被单独分流到一个 conditionalSkills Map:

// Separate conditional skills (with paths frontmatter) from unconditional ones
for (const skill of deduplicatedSkills) {
  if (skill.type === 'prompt' && skill.paths && skill.paths.length > 0
      && !activatedConditionalSkillNames.has(skill.name)) {
    newConditionalSkills.push(skill)
  } else {
    unconditionalSkills.push(skill)
  }
}

// Store conditional skills for later activation when matching files are touched
for (const skill of newConditionalSkills) {
  conditionalSkills.set(skill.name, skill)
}

// 最后只返回无条件的 Skill
return unconditionalSkills

这段逻辑的含义是:带 paths 的 Skill,根本不会像普通 Skill 一样直接进入启动时上下文。它会先待在一个“条件激活区”里,只有当你在会话里碰到了匹配路径的文件,它才会被真正激活。

这点对复杂项目非常有价值。

比如你给支付模块写一个 paths: src/payment/** 的 Skill,在你处理用户系统、文章系统、管理后台时,这个 Skill 对 Claude 几乎是隐身的。只有当你真的进入 src/payment/ 相关文件,它才“出现”。

这也是我现在很认同的一种团队实践:不要在根目录堆一个什么都想管的大 Skill 集合,而是让复杂模块在自己的目录附近维护自己的 Skill。

4. 为什么大型项目不该只在根目录维护一套总 Skill

还有一个很容易被忽略,但工程上非常实用的机制:Claude Code 会从当前文件所在目录一路向上寻找 .claude/skills

源码大概是这样:

// Walk up to cwd but NOT including cwd itself
while (currentDir.startsWith(resolvedCwd + pathSep)) {
  const skillDir = join(currentDir, '.claude', 'skills')
  // ...check if exists, then load
  currentDir = dirname(currentDir)
}

// Sort by path depth (deepest first) so skills closer to the file take precedence
return newDirs.sort((a, b) => b.split(pathSep).length - a.split(pathSep).length)

这里最关键的是最后一行:deepest first。也就是说,越靠近当前文件的 Skill,优先级越高。

这意味着你放在 src/auth/.claude/skills/ 里的 Skill,可以自然覆盖根目录下更通用的同名 Skill。对 monorepo 或大仓库来说,这个机制非常好用:

  • packages/api/.claude/skills/ 可以放 API 专属 Skill
  • packages/web/.claude/skills/ 可以放前端专属 Skill
  • 根目录只保留真正的全局规则

如果把上面四点放在一起看,设计 Skill 的顺序其实会变得很清楚:

  • 先把 frontmatter 写准,再去打磨正文步骤
  • 先按最小权限收紧 allowed-tools,再考虑要不要给更多能力
  • 只有模块边界明确时再上 paths,不要为了“高级”硬加
  • 多目录项目优先做“离代码更近”的局部 Skill,而不是维护一个大而全的总模板

5. 为什么长对话里,Skill 不会轻易“失忆”

还有一个很多人会担心的问题:会话一长、上下文一压缩,前面调过的 Skill 会不会就悄悄失效了?

从实现思路看,Claude Code 不是简单把它们扔掉,而是会把最近调用过的 Skill 重新注入压缩后的上下文。工程上你可以把它理解成:Skill 不是“一次触发完就全靠模型自己记住”,而是一个可以被系统再次带回来的工作单元。

当然,这也不是说你可以无限制地把 Skill 写成超长文档。实现上会有保留预算,比如最近调用的 Skill 只会保留前一段核心内容,而不是把所有正文永久塞在上下文里。所以前面那条原则依然成立:把 frontmatter 写准,把正文写短,把真正长的材料拆到 reference 或脚本里。

Skill 运行机制与条件激活


七、不是所有 Skill 都应该让 Claude 自动触发

到这里,其实已经够你写出一个基础可用的 Skill 了。

但如果你真的准备在项目里长期用,接下来有一个问题迟早会遇到:

这个 Skill 到底应该让 Claude 自动触发,还是只能我手动触发?

这背后其实对应两种完全不同的 Skill。

1. 参考型 Skill:给 Claude 补充背景知识

这类 Skill 的作用不是“执行一个任务”,而是“把某个项目知识注入到当前工作里”。

比如 API 设计规范、错误处理约定、数据库命名规则。这些东西你希望 Claude 在写代码、改代码、review 代码时,只要场景合适就自动想起来。

这类 Skill 的特点是:

  • 倾向自动触发
  • 内容会留在主对话上下文里
  • 更像“局部规范”而不是“独立任务”

比如:

---
name: api-conventions
description: API design patterns and conventions for this codebase. Use when writing or reviewing API endpoints.
---

响应格式统一用 { success, data, timestamp }
禁止在 controller 层直接写 SQL,通过 service 层操作
所有异步函数必须有 try-catch,错误统一 throw new AppError()

2. 任务型 Skill:给 Claude 一个要完成的动作

这类 Skill 有明确边界,通常还可能带副作用。

比如 /deploy/send-release-email/prepare-release-notes/migrate-db。这类 Skill 更像一段可执行流程,而不是知识注入。

这类 Skill 的特点是:

  • 通常应该手动触发
  • 有副作用时最好加 disable-model-invocation: true
  • 有风险时再配 context: fork

比如:

---
name: deploy
description: Deploy the application to production. Manual trigger only.
context: fork
disable-model-invocation: true
allowed-tools:
  - Bash(npm run test)
  - Bash(npm run build)
  - Bash(git push *)
---

1. 跑完整测试:npm run test
2. 确认测试全绿后构建:npm run build
3. 推送到部署分支
4. 等待 CI 完成并检查健康状态

这里的关键不是字段多了,而是触发权变了。

你真正要想清楚的问题是:

这件事我愿不愿意让 Claude 自己判断“现在该触发了”?

如果答案是“不愿意”,那就不要让它自动触发。

3. disable-model-invocation: trueuser-invocable: false 不是一回事

这是一个很容易混淆,但又非常关键的区别。

默认情况下,你和 Claude 都可以调用一个 Skill。你可以手动 /skill-name,Claude 也可以在觉得合适时自动加载它。

但很多人会把下面两个字段混为一谈:

  • disable-model-invocation: true
  • user-invocable: false

它们看起来都像“限制调用”,其实限制的是两件完全不同的事。

disable-model-invocation: true 的意思是:Claude 不能自己触发,只有你能手动触发。 这类 Skill 适合部署、发版、发邮件、推送消息这类你必须自己掌握时机的动作。更重要的是,它还会把这个 Skill 的描述从 Claude 的常驻上下文里拿掉,平时的上下文成本直接归零,只有你手动调用时才完整加载。

user-invocable: false 的意思则是:你不能从 / 菜单里把它当命令来点,但 Claude 仍然可以在合适时自动用它。 这类 Skill 更适合背景知识,比如老系统架构、内部缩写、遗留约定。这些东西你希望 Claude 在相关任务里自动想起来,但你并不需要一个显眼的 /legacy-context 命令天天挂在菜单里。

所以更准确的判断应该是:

  • 有副作用、要你亲自控制时机:disable-model-invocation: true
  • 只是背景知识、不适合被人手动当命令点:user-invocable: false
  • 想限制 Claude 到底能不能调用某些 Skill:去配权限规则,而不是只盯菜单显示

这组区别值得写进脑子里,因为它直接决定了 Skill 的“触发权”到底属于谁。

参考型 Skill 与任务型 Skill


八、Skill 的天花板:从静态模板到可执行能力

前面几节讲的,主要还是“怎么把经验写成一个好模板”。但如果 Skill 只能放静态文字,它的上限其实并不高。

真实项目里,很多任务依赖的是实时信息:当前 PR 的 diff、评论区讨论、今天的测试结果、最新 schema、CI 状态。你当然可以在 Skill 里写“先去看这些东西”,但这样一来,最关键的一步又变回 Claude 自己先兜一圈去找。

Claude Code 里真正更有意思的地方是:Skill 不只是 prompt 模板,它还可以在触发瞬间先准备上下文。

1. 动态注入:先拿真实数据,再交给 Claude

比如你要做一个 PR 总结 Skill,不一定要让 Claude 先自己去猜该看哪些信息,你可以直接让 Skill 触发时先把它们准备好:

---
name: pr-summary
description: Summarize the current pull request. Use when asked to review or summarize a PR.
context: fork
allowed-tools:
  - Bash(gh *)
---

## 当前 PR 信息
- diff:!`gh pr diff`
- 评论区讨论:!`gh pr view --comments`
- 涉及文件:!`gh pr diff --name-only`

## 你的任务
基于以上真实数据,总结这个 PR 做了什么、为什么改、有哪些潜在风险。

这个 Skill 真正触发时,前面的命令会先执行,输出直接注入到 prompt 里。Claude 拿到的不是“请你去看看 PR”这种模糊要求,而是已经准备好的真实上下文。

这也是为什么前面说 scripts/ 不是装饰。如果一个 Skill 需要先查状态、先取数、先做一轮预处理,那它就不再只是“告诉 Claude 怎么做”,而是在执行前把材料也一起备好了。更复杂一点时,你完全可以把逻辑放进 scripts/,再通过 CLAUDE_SKILL_DIR 去调用目录里的脚本,让 Skill 触发时先跑一轮取数或整理。

2. 这件事为什么重要:它决定了 Skill 的上限

一旦你理解了动态注入这层能力,就会发现 Skill 的上限根本不只是“几行 prompt”。

它可以同时承担三件事:

  • 定义触发条件
  • 限制工具权限
  • 在执行前准备实时上下文

换句话说,Skill 不是只能做静态模板,它完全可以长成一个带入口、带约束、还能主动取数的局部能力单元。

不要只把 Skill 当成“写给模型的一段话”,而要把它当成“一个局部工作流的入口”。

3. 从架构位置看,Skill 不是 Tool,也不是 Agent

如果再往上抽一层,我现在对 Skill 的理解是:

  • Tool 是原子能力
  • Skill 是任务知识和操作规约
  • Agent 是执行与编排单元

这个分层不一定是官方唯一表述,但作为工程心智模型非常有用。因为一旦把 Skill 放到 Tool 和 Agent 中间去理解,很多判断都会顺下来:

  • 该写成脚本的,别硬写进 Skill 正文
  • 该放进 CLAUDE.md 的全局规则,别误沉淀成任务 Skill
  • 该拆给 SubAgent 的复杂协作,别硬让一个 Skill 扛完

Skill 真正做的事,不是替代 Tool,也不是替代 Agent,而是把“什么场景下、按什么步骤、调用哪些能力”组织成可复用的任务单元。


九、真正让 Skill 变靠谱的,不是写出来,而是验证出来

这是我觉得官方 best practices 里最容易被忽略、但最能拉开水平差距的一点。

很多人写 Skill 的方式是:先凭感觉写一版,再上项目里试试。这样当然也能跑,但它很容易陷进一种错觉里: 你以为自己在“优化 Skill”,其实只是一直在补想象中的问题。

官方更推荐的思路其实更工程化:

先准备评测样例,再写 Skill。

最轻量、也最实用的做法,是先找出三个真实场景:

  • 一个 Claude 原本就能做得不错的
  • 一个 Claude 容易漏步骤的
  • 一个 Claude 容易理解偏、触发错或者输出不稳的

先不用 Skill 跑一遍,记录基线。看它具体错在哪:

  • 是没想到要用这个 Skill
  • 是触发了,但没读对参考资料
  • 是步骤顺序不稳
  • 是输出格式飘了
  • 是该脚本处理的确定性工作,被它自己“猜”过去了

然后再写最小版本的 Skill,只补刚才暴露出来的那几个缺口,而不是一上来把所有可能性都写满。

把这个过程压缩成一句话,其实就是:

  1. 先找失败样例
  2. 再写最小 Skill
  3. 再看它是不是真的修掉了失败样例

如果三轮下来都没有明显提升,那大概率不是 Skill 写得不够多,而是这个问题根本不该沉淀成 Skill。

不要只看结果,还要看 Claude 是怎么“导航”这个 Skill 的

只看最终结果对不对还不够,更重要的是看 Claude 在过程中到底是怎么用这个 Skill 的。

官方专门建议观察 Claude 实际怎么使用 Skill,而不是只看最终结果对不对。比如:

  • 它是不是总在错误顺序里读文件
  • 它是不是老是忽略某个引用文件
  • 它是不是每次都反复读同一个 reference,那这部分也许该往 SKILL.md 主体里提
  • 某个 examples 文件是不是从来没被读过,那它可能根本没价值,或者信号太弱

这些观察特别重要,因为它能直接反推出信息架构是不是合理。

我现在会把一个 Skill 是否成熟,简单看成四个问题:

  • 会不会在该触发的时候触发
  • 触发后会不会按预期去读材料
  • 执行过程会不会漏掉关键步骤
  • 输出结果能不能稳定复现

如果你团队里会混用不同模型,官方还建议至少跨你计划使用的模型测一遍。不是因为所有模型都得兼容,而是因为有些 Skill 对提示强度和结构依赖更高,换模型后会暴露出你原来没看到的问题。

说到底,Skill 的成熟度,不是靠“我觉得写得挺全”来判断,而是靠一组真实任务能不能稳定跑通来判断。


十、写 Skill 时,最常见的四种设计模式,以及最容易踩的反模式

这里先说清楚:下面这四种名字,不是官方文档逐字给出的固定术语,而是我结合 Anthropic 官方 best practices 做的工程化归纳。

但它们确实能覆盖大多数项目里真正会遇到的 Skill 设计问题。

1. 模板驱动模式:解决“输出不稳定”

这类 Skill 的核心不是让 Claude 更会想,而是让它更稳定地按结构输出。

适合:

  • 周报
  • PR 描述
  • 事故复盘
  • 评审报告

它的关键不是“给一个模板”,而是把模板当成输出接口。模板负责格式,SKILL.md 负责路由和规则。模板里不要塞判断逻辑,也不要把一个模板写成一套小程序。

如果一个模板已经长到一百多行,而且开始出现大量条件分支,通常不是你模板写得认真,而是职责已经混了。

2. 脚本增强模式:解决“结果不稳定”

这类 Skill 的核心是:确定性的事交给脚本,不要交给模型猜。

适合:

  • 指标统计
  • CSV 解析
  • 正则匹配
  • Git / PR / CI 状态抓取
  • 需要预处理的上下文准备

官方对这件事的说法很直白:solve, don't punt。能脚本算出来的,就别只写一句“请 Claude 自行分析”。

这类 Skill 真正提高的,不是文风,而是可靠性。你把概率型推理替换成确定性执行,整个 Skill 的下限会明显抬高。

3. 知识分层模式:解决“上下文过载”

这是官方反复强调的 progressive disclosure 思路。

也就是:SKILL.md 只做入口和导航,把大块知识拆进 reference/examples/forms/ 这类文件里,按需读取,而不是一次灌满。

这类模式适合:

  • 领域知识很多的 Skill
  • 不同子领域差异很大的 Skill
  • 需要高级用法、边界情况、案例库的 Skill

它的关键不是“拆文件”,而是“拆得有层次”。官方明确不建议深层嵌套引用。最稳的做法是所有重要材料都从 SKILL.md 一层直达,别让 Claude 从 advanced.md 再跳 details.md 才看到真正关键信息。

4. 工具隔离模式:解决“能力边界失控”

这类模式最容易被低估。

很多人写 Skill 时只关注“让它能做事”,但真正稳定的 Skill 往往同样重视“让它不能乱做事”。

适合:

  • 部署
  • 发版
  • 写数据库迁移
  • 调外部系统
  • 会改文件、发消息、推远端的任务

这一类 Skill 的核心组合通常是:

  • allowed-tools 收到最小
  • 必要时配 context: fork
  • 不希望自动触发时加 disable-model-invocation: true

它不是在限制 Claude 的创造力,而是在设计这个能力包的安全边界。

最常见的反模式

如果把前面四种模式反过来看,最常见的坑基本也就集中在下面这些地方:

  • 一个 Skill 管太多事,什么都想覆盖,最后什么都不够准
  • description 写得很空,只写“处理文档”“帮助分析”这种谁都能套上的话
  • 一上来给一堆方案,不给默认路径,让 Claude 自己从五六种做法里摇摆
  • 模板里写判断逻辑,或者把本该脚本做的事丢给模型推理
  • reference 层级太深,真正关键信息藏在第二跳、第三跳文件里
  • 把会过期的信息硬写进 Skill,比如时间敏感规则、旧接口切换说明
  • 默认假设工具和依赖都已经装好,结果一跑就断
  • 在路径里写 Windows 反斜杠,跨环境直接出问题

你会发现,所谓反模式,本质上就是一句话:

该约束的地方没约束,该拆开的地方没拆开,该交给脚本的地方还在让模型猜。


十一、完整案例:把一个通用 code review 模板,提炼成你项目真正需要的 Skill

上面说了这么多抽象原则,不如走一遍完整例子。

假设你们团队每周都做后端代码审查,而且总在重复盯这几件事:

  • 有人改一个功能,顺手动了三个不相关模块
  • 新同学不知道项目里统一用 AppError,直接 throw new Error()
  • Promise 链里漏了 await
  • 数据库查询没有索引,或者潜在 N+1 没被看出来

这就是非常典型的“该沉淀 Skill 的信号”。

第一步:先确认痛点到底是什么

这一步别着急写模板,先把“你们到底在反复出什么问题”说清楚。

很多团队的问题不是“没有 code review”,而是每次 review 的注意力都被分散了。真正高频出错的点,永远是那几类项目特有的约束。

所以要沉淀的不是“代码审查”这四个字,而是你们团队在代码审查里最容易漏掉的那几类检查。

第二步:从公开模板里提取真正有用的部分

这时候公开模板就有用了,但它的用途不是直接上生产,而是当素材库。

假设你找到一个 50 行的通用 code review 模板。你真正该提取的,可能只有下面这几类东西:

  • 逻辑正确性,尤其是异步操作
  • 项目约定的遵守,比如 AppError、错误处理模式
  • 数据库相关的风险,比如 N+1、索引、查询范围
  • 改动范围是否聚焦,不要顺手改不相干文件

剩下那些跟你们项目关系不大的部分,就应该果断删掉。

第三步:把它压缩成一个真正能用的 Skill

最后落地出来的 Skill,应该更像这样:

---
name: code-review
description: Review TypeScript backend code before merging. Use when asked to review code, check a PR, or verify implementation before committing.
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash(git diff *)
argument-hint: "[PR 分支名或文件路径]"
---

# Code Review Skill

## Steps
1. 读 $ARGUMENTS 的改动 diff,确认改动是否只涉及这个 PR 的范围(不要顺手改无关文件)
2. 检查错误处理:所有 throw 都必须是 throw new AppError(),不能 throw new Error()
3. 检查异步操作:Promise 链是否有遗漏的 await,错误是否被正确 catch
4. 检查数据库查询:是否有 SELECT * 的懒惰写法,是否明显的 N+1 查询,关键查询是否 explain 过

## Output Format
Issues found (Critical → Warning → Info):

🔴 **Line 45**: Missing `.select()` in Prisma query - this will fetch unnecessary columns
🟡 **Line 67**: Potential N+1: loop inside `posts.map()` should use `Promise.all()`**No AppError violations** — all errors properly handled

Summary: 1 critical issue to fix before merge.

## Caveats
- 不审查 UI 层代码(只关心后端逻辑)
- 不关注代码风格(那是 prettier 的事)
- 如果一个改动涉及多个不相干功能,分别提交 PR 再 review

你会发现,到这一步之后,Skill 就不再是“通用模板的中文版”了,而是你们项目真正有用的一个局部工作流。

50 行公开模板,最后可能只剩下 4 个真正属于你项目的核心关注点。但恰恰是这 4 个点,才决定它到底值不值得用。

第四步:在真实使用里继续迭代

Skill 从来不是一次写完的。

比如你用了两周之后,又发现一个常见问题:改了数据库 schema,但忘记更新 Prisma 类型。那就把它加进去:

4.5. 检查 Prisma 类型:如果改了数据库,Prisma schema 和生成的 types 是否都已更新

这时候你会发现,Skill 真正的价值不是“第一次写出来”,而是在真实工作里被持续打磨。


十二、Skill 的维护节奏,比第一次写出来更重要

Skill 不是写好就扔。

如果你写完之后三个月不看,它很快就会从“项目经验”重新退化成“历史遗留文档”。

我更推荐一个更贴近实际的轻量节奏:

前一两周
高频使用,快速迭代。每次用完就问自己三个问题:步骤是不是太复杂?输出是不是太啰嗦?有没有漏掉今天刚踩到的新坑?

稳定之后每周一次
回顾一次。看看最近有没有经常被遗漏的步骤,有没有新的痛点需要加入。对 Skill 这种高频小迭代的东西来说,按周看通常比按月看更合适。

每个月做一次清理
把已经不再是问题的注意事项删掉,把那些已经变成全局共识的规则移进 CLAUDE.md。这一步的重点不是加内容,而是防止 Skill 越写越胖。

Skill 应该越用越精炼,而不是越写越臃肿。


十三、一个特别反直觉,但很重要的经验:第一个 Skill,故意别写最重要的任务

这条我非常想单独拿出来讲。

因为很多人第一次沉淀 Skill,会本能地想挑一个最关键的任务,比如“生产环境发布前检查”“数据库迁移前审查”“支付流程改动 review”。

但工程上更稳的做法,其实正好相反。

大多数人写的第一个 Skill,质量都不会太高。触发条件偏模糊,步骤偏啰嗦,输出格式也不够稳定。这很正常,因为你第一次做这件事时,对“什么是好的 Skill”还没有直觉。

如果你一开始就把它用在最关键的任务上,一旦写得不够好,伤害会非常直接:要么关键场合出问题,要么你从此对这套机制失去信心。

更好的策略是:先拿一个重要程度中等、容错率比较高的任务练手。

比如:

  • 写周报
  • 生成 PR 描述
  • 做一次常规 code review

先跑两周,迭代两三次,等你对 Skill 的节奏有感觉了,再去沉淀真正关键的流程。

第一个 Skill 的目的,不是直接解决最大的问题,而是让你学会怎么写 Skill。


十四、如果你今天就想开始,可以直接做这三个动作

别先想着搭一整套系统,直接从最小动作开始:

任务一: 列出你最近一个月里重复做过三次以上的任务。用第一节的判断法(对所有任务成立?→ CLAUDE.md;只对某类任务成立?→ Skill;只对这次成立?→ Prompt),确认你要处理的是 Skill 还是 CLAUDE.md 的事。

任务二: 为这个任务写一个 Skill。先想清楚四个问题(什么时候用、按什么顺序、输出长什么样、什么时候不适用),然后包装成 SKILL.md。目标是精炼——大多数好 Skill 的有效指令不超过 10 行。记住:第一次的目的是练手,不是写出完美的 Skill。

任务三: 选三个你最近真的遇到过的场景,先不用 Skill 跑一遍,再用 Skill 跑一遍,对比它到底有没有少漏步骤、少返工、少补充解释。然后在 SKILL.md 里补一行今天发现的问题(哪个步骤漏了,或者哪个注意事项需要加)。这就是 Skill 的维护节奏,也是最轻量的评测方法。

真正好的 Skill,几乎都不是第一次就写对的,而是在实际使用里慢慢长出来的。


下篇预告

第 06 篇:Sub-agents 实战——什么时候应该拆任务,怎么设计子任务边界

单个 Claude 实例有上下文上限,复杂任务拆成多个子任务让 Sub-agents 并行处理,理论上能大幅提速。但什么时候值得拆?拆错了会有什么代价?下一篇会拆开 Sub-agents 的真实适用场景,以及最常见的过度设计陷阱。


写在最后

公开 Skill 模板当然有用,但它的价值更像脚手架,而不是成品。

你不需要一个很复杂的 Skill 系统。你需要的,往往只是把团队最容易反复犯错的那几件事提前写下来,让 Claude 每次都替你盯住。

如果你现在想动手,就直接做这三步:列出最近一个月里重复做过三次以上的任务;用"三问法"确认它该放 CLAUDE.mdSkill 还是 Prompt;先写一个只有 5 到 10 行的版本,实际用一次,再立刻改第一轮。不要追求第一版完美——Skill 的价值,从来不是"写出来的那一刻",而是"它开始帮你减少重复错误的那一天"。

评论区想聊一个问题:你现在最卡的,到底是"写不出规则",还是"没分清哪些该放 CLAUDE.md、哪些该做成 Skill"? 这两个问题看起来很像,但解法完全不同。

觉得这篇有帮助,点个赞让更多工程师看到 👍


AI Coding 系列持续更新。用别人的 Skill 模板是起点,不是终点。真正管用的 Skill,只有你自己的项目才能提炼出来。

AI编程时代解决bug的新业态

本文是想通过一个例子来讲述,AI在修复Bug方面令人惊艳的能力。

一、传统方式下

先来看一个Crash日志的堆栈信息:

Termination Reason:<RBSTerminateContext| domain:10 code:0x8BADF00D 
explanation:scene-create watchdog transgression: application<com.xxx.aaa>:
34689 exhausted real (wall clock) time allowance of 3.43 seconds

// 
Thread 0 Crashed:
0      libsystem_pthread.dylib       _pthread_mutex_lock$VARIANT$armv81 + 120
1      libc++.1.dylib                std::__1::mutex::lock() + 12
2      libicucore.A.dylib            icu::Locale::getDefault() + 32
3      libicucore.A.dylib            icu::Locale::init(char const*, signed char) + 1400
4      libicucore.A.dylib            _ures_getLocaleByType + 436
5      libicucore.A.dylib            icu::DecimalFormatSymbols::initialize(icu::Locale const&, UErrorCode&, signed char, icu::NumberingSystem const*) + 256
6      libicucore.A.dylib            icu::DecimalFormatSymbols::DecimalFormatSymbols(icu::Locale const&, icu::NumberingSystem const&, UErrorCode&) + 236
7      libicucore.A.dylib            icu::number::LocalizedNumberFormatter::getDecimalFormatSymbols() const + 4608
8      libicucore.A.dylib            icu::number::LocalizedNumberFormatter::getDecimalFormatSymbols() const + 1632
9      libicucore.A.dylib            icu::number::LocalizedNumberFormatter::formatImpl(icu::number::impl::UFormattedNumberData*, UErrorCode&) const + 128
10     libicucore.A.dylib            icu::SimpleDateFormat::zeroPaddingNumber(icu::NumberFormat const*, icu::UnicodeString&, int, int, int) const + 524
11     libicucore.A.dylib            icu::SimpleDateFormat::subFormat(icu::UnicodeString&, char16_t, int, UDisplayContext, int, char16_t, icu::FieldPositionHandler&, icu::Calendar&, UErrorCode&) const + 904
12     libicucore.A.dylib            icu::SimpleDateFormat::_format(icu::Calendar&, icu::UnicodeString&, icu::FieldPositionHandler&, UErrorCode&) const + 688
13     libicucore.A.dylib            icu::SimpleDateFormat::format(icu::Calendar&, icu::UnicodeString&, icu::FieldPosition&) const + 80
14     libicucore.A.dylib            icu::DateFormat::format(double, icu::UnicodeString&, icu::FieldPosition&) const + 124
15     libicucore.A.dylib            _udat_format + 356
16     CoreFoundation                ___cficu_udat_format + 64
17     CoreFoundation                _CFDateFormatterCreateStringWithAbsoluteTime + 180
18     Foundation                    -[NSDateFormatter stringForObjectValue:] + 160
19     MyAPP                        -[HAMLaunchMonitor startUUID] + 136
...    MYApp                               其它调用函数

传统解决步骤:

  1. 查看HAMLaunchMonitor中startUUID方法里面内容,看代码上有没明显bug;
  2. 查看调用NSDateFormatter的string...方法的方式是否有问题;
  3. 后面是系统堆栈,所以得出结论是系统库发生了长时间等锁;
  4. 查看崩溃说明是0x8BADF00D,确定是被系统WatchDog检测到杀死了进程导致的崩溃。
  5. 解决方式:少量偶现且逻辑不好修改,出临时性修复方案~
  6. 问题可能没有根本解决,只是概率降低了。

可以看到我们人工查看这个问题有几个瓶颈:

  1. 需要手动找到文件查看分析函数调用链,速度慢。
  2. 对于顶部的系统函数,我们一般是缺乏了解的,不知道里面发生了什么导致出了问题。
  3. 弄清楚了上面内容后,我们的解决方法也是基于自己认知的,存在偏差。

而这几个瓶颈,交给AI后那都不是事儿。

二、AI时代下Bug修复

复制堆栈信息交给AI工具(我目前用的是Qoder),AI直接给出了根本原因及有效的方案:

Crash 类型

这是一个 Watchdog 超时 Crash0x8BADF00D = "ate bad food"),发生在 scene-create(应用启动/场景创建) 阶段。系统允许的时间为 3.43 秒,但应用在该时间内未能完成启动。

根因分析

从调用栈来看,崩溃发生在 [-[HAMLaunchMonitor startUUID]] 方法中:

- (NSString *)startUUID { 
    NSDateFormatter *formatter = [NSDateFormatter new];
    formatter.dateFormat = @"yyMMddHHmmssSSS";
    formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; 
    NSString* time = [formatter stringFromDate:[NSDate date]]; 
    // ... 
} 

主线程被阻塞在 pthread_mutex_lock,具体路径是:

  1. startUUID 调用 NSDateFormatter stringForObjectValue: 进行日期格式化
  2. 底层 ICU 库执行 SimpleDateFormat::format 进行实际格式化
  3. ICU 在格式化数字时需要获取 DecimalFormatSymbols
  4. 这触发了 icu::Locale::getDefault() 来获取系统默认 locale
  5. getDefault() 内部需要获取一个 全局互斥锁(std::mutex
  6. 该锁被其他线程持有,导致主线程死等(pthread_mutex_lock 卡住)

核心问题

锁竞争(Lock Contention) —— ICU 库内部有一个全局的 std::mutex 保护 Locale::getDefault()。在应用启动阶段,如果有多个线程同时使用 NSDateFormatterNSNumberFormatter 或其他依赖 ICU 的 API,就会争抢这把锁。主线程被阻塞等锁,累计时间超过 watchdog 限制,系统杀掉了应用。

加剧因素

  1. 启动阶段调用 startUUID:该方法在 [commonParameters] 中被调用,而 commonParameters 在 [appDidFinishLaunch] 流程中执行——正处于 watchdog 监控最严格的时间窗口。
  2. 每次调用都创建新的 NSDateFormatterNSDateFormatter 的初始化本身就是重量级操作,且会触发 ICU locale 的初始化。
  3. ICU 内部的全局锁:这是系统库的已知问题,在 iOS 启动早期阶段锁竞争尤为严重,因为各种系统框架也在并发初始化。

建议修复方案

  1. 缓存 NSDateFormatter(最直接有效):将 NSDateFormatter 创建为静态变量或单例属性,避免每次调用 startUUID 都重新创建:
- (NSString *)startUUID {
 static NSDateFormatter *formatter = nil;
  static dispatch_once_t onceToken; 
  dispatch_once(&onceToken, ^{
   formatter = [NSDateFormatter new]; 
   formatter.dateFormat = @"yyMMddHHmmssSSS"; 
   formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; 
  }); 
   NSString *time = [formatter stringFromDate:[NSDate date]]; 
   NSString *startUuid = [NSString stringWithFormat:@"%@2000000%@", time, [self getRandomNumber]]; 
   return startUuid; 
 } 
  1. 延迟执行:将 startUUID 和上报逻辑延迟到首帧渲染之后,脱离 watchdog 监控的关键时间窗口。
  2. 替代方案:如果仅需要生成唯一标识,可以完全避免 NSDateFormatter,改用时间戳数字拼接: 这完全绕开了 ICU/NSDateFormatter,不存在锁竞争问题。
- (NSString *)startUUID { 
NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; 
long long ms = (long long)(now * 1000); 
NSString *startUuid = [NSString stringWithFormat:@"%lld2000000%@", ms, [self getRandomNumber]]; 
return startUuid;
 } 

推荐方案 3,因为它从根本上消除了 ICU 锁竞争的风险,同时性能开销最小。

三、感想

AI时代,整体APP行业的性能将得到大幅提升

优秀大模型解决Bug的能力很强,且修复速度和质量都发生了颠覆性的变化,意味着如果有规划+APP开发人员有心,大部分问题都能解决;AI编程工具下,目前AI写复杂功能可能还有点问题,但如果是指定AI去写一个特定功能bug率可能会比资深工程师还要好;借助AI来深入了解底层知识也很方便,对于提升工程师认知也有帮助,进一步提升了性能。

AI时代,Bug的解决方式会发生变化

现在的热修复功能集成到APP后,往往需要编写修复后的脚本语言文件,下发到APP,APP动态运行时交换方法实现解决。

AI时代的方式可能是:
-》Crash发生后,自动分析原因,出解决方案,发出通知;
-》人工收到通知后,选择一个方案;
-》自动生成对应的脚本文件,自动下发到对应的APP版本。
-》APP再次打开时,Bug已经自动修复。

VTJ.PRO 发布 v2.3.6:开放共享模版、优化发布流程,低代码开发体验再升级

摘要: 基于 Vue 3 的开源 AI 低代码平台 VTJ.PRO 于 2026 年 4 月 10 日正式发布 v2.3.6 版本。本次更新聚焦模版共享与发布体验,开放共享模版功能,整合发布操作链路,并优化了版本控制与自动化截图能力,进一步降低了项目复用与协作的门槛。

未命名.png


开放共享模版,构建可复用的组件生态

v2.3.6 最值得关注的变化是 开放共享模版功能。开发者现在可以将自己设计的页面、模块或完整应用打包为模版,并发布到共享空间供团队或社区复用。同时,发布模版的版本控制机制得到强化,解决了此前模版更新失败的问题,使模版的迭代和回滚更加可靠。

  • 发布模版后,系统会自动在开发项目中创建对应的模版引用页面,实现“一次发布,处处引用”。
  • 模版共享结合原有的 AI 能力(设计稿转代码、自然语言生成页面),可大幅提升团队内部标准化组件的沉淀效率。

发布操作统一化,支持自动截图

为了减少多入口切换的认知负担,新版本将 发布应用、发布模版、项目出码 三个核心操作按钮整合至同一界面,开发者无需在不同菜单间跳转即可完成完整的交付流程。

此外,发布应用现已支持自动生成截图。系统会在发布时自动捕获当前应用界面的关键视图,方便在版本记录、发布日志或团队协作中快速识别应用状态。

默认公开与取消自动启动页,更贴合实际开发习惯

  • 创建应用时,访问权限默认为“公开”。这一调整降低了团队内部或开源项目中的分享门槛,同时也保持了随时可改为私有的灵活性。
  • 取消创建应用时自动新增启动页。此前新建应用会自动生成一个示例启动页,部分开发者反馈会带来额外的删除操作。新版本不再自动生成启动页,应用创建后直接进入空白设计状态,更符合从零开始的开发直觉。

开发者体验:从“可用”到“好用的低代码”

VTJ.PRO 一直强调 “降低复杂度,不降低自由度” ,v2.3.6 的更新再次印证了这一理念——通过优化发布链路和模版共享能力,让团队协作中的资产复用更加自然,同时保持对 Vue 源码的完全控制。

目前,VTJ.PRO 已在 Gitee 收获 9.9K Star,荣获 Gitee 2025 年度“大前端 Top3”。项目基于 Vue 3 + TypeScript + Vite,深度集成 ElementPlus、ECharts 等主流库,并已接入 DeepSeek、Qwen、Gemini、GPT 等 10+ 款大模型。

快速体验与更新方式


关于 VTJ.PRO
VTJ.PRO 是一款开源、基于 Vue 3 的 AI 低代码开发平台,支持可视化设计与手写代码双向转换,并提供私有化部署、多端输出(Web、H5、UniApp)、版本管理与企业级协作能力。项目始终保持“源码透明、无黑盒锁定”,是面向专业开发者的低代码解决方案。

11.png

连载04-最重要的Skill---一起吃透 Claude Code,告别 AI coding 迷茫

screenshot-20260410-194956.png

别再直接 Fork 别人的 Claude Skill:公开模板只是原材料,项目规则才是成品

AI Coding 系列第 05 篇 · 核心工具

我第一次批量导入公开 Skill 模板的时候,是真的以为自己走了捷径。

GitHub 上一堆 star 很高的仓库,code review、需求分析、文档编写、调研、拆任务,看起来什么都有。我当时的想法特别简单:既然别人已经把常见工作流整理好了,我直接 fork 一份,全量导入,不就能让 Claude 立刻更稳、更懂项目吗?

结果用了几天,我反而越来越不放心。

它每次都输出得很像那么回事。格式完整,措辞专业,检查项也不少。可真正让我在项目里反复吃亏的那几件事,它一次都没替我盯住。异步链里是不是又漏了 await,这次 migration 有没有回滚方案,新同学是不是又顺手写了 throw new Error(),数据库 schema 改了之后 Prisma 类型是不是也一起更新了。

它会提醒一堆“大家普遍都应该注意”的东西,却不懂“我们团队到底最怕什么”。

后来我才慢慢明白,问题不是 Skill 机制不好,而是我导入的根本不是自己的 Skill,只是别人整理好的经验。

这些经验当然有用,但它们解决的是共性问题,不会天然长成你项目里的“肌肉记忆”。

这篇文章就讲一件事:

怎么把公开模板当原材料,而不是成品;怎么从自己的项目里,提炼出一个真正会被反复复用、而且越用越准的 Skill。

如果你只想先记住一句话,那就是:

公开 Skill 模板是素材库,不是最终产品。


先给你一个判断框架

为了后面不绕,我先把最核心的判断放前面:

  • 对所有任务都生效的规则,放 CLAUDE.md
  • 只对某一类任务生效的规则,做成 Skill
  • 只对这一次任务有效的约束,写进 Prompt
  • 只有“输入相对稳定、输出有共同模式、而且容易漏步骤”的任务,才值得沉淀成 Skill
  • 第一个 Skill 不要选最关键的任务,先拿中等风险任务练手

如果你现在就卡在“这条到底该写哪”,后面大部分内容其实都可以用这五条往回推。


一、为什么很多公开 Skill 模板,一开始觉得香,后来却越用越别扭

我现在反而会对“看起来很全”的公开 Skill 模板保持一点警惕。

不是因为它们没用,而是因为它们太容易制造一种错觉:好像什么都覆盖到了,但真正最重要的东西其实没进去。

公开模板最常见的问题,不是方向错,而是下面这三种。

1. 太宽泛

它什么都管一点,但什么都不够深。

它会告诉你“注意异常处理”“注意性能”“注意安全”,这些当然没错。但这些话本身不构成你项目里的工作流。它不知道你们统一用的是 AppError,不知道你们数据库变更必须检查回滚,也不知道你们哪几个目录历史包袱最重。

2. 太嘈杂

50 行模板里,真正有价值的可能只有 5 行。

剩下的 45 行不是完全没用,而是在和那 5 行争夺 Claude 的注意力。对于 agent 来说,规则不是越多越强。很多时候,8 行写透项目约束的 Skill,比 50 行“样样都提一点”的模板更有用。

3. 太不像你的项目

这点才是最致命的。

公开模板知道“大家普遍应该注意什么”,但不知道“你们团队反复死在哪些地方”。而真正有价值的 Skill,恰恰应该把那些项目特有、团队高频踩坑的东西固化下来。

说得更直白一点:你把一个新同事扔进团队,给他一份行业通用培训材料,当然比什么都不给强;但如果你不告诉他“我们团队最容易出错的是哪三件事”,他依然干不好你最在意的活。

所以正确姿势不是“找一个最全的模板直接用”,而是:

先借鉴,再裁剪,最后只留下真正属于你项目的那几条。


二、先把 Prompt、CLAUDE.md、Skill 这三件事分清楚

很多人不是不会写 Skill,而是一开始就把这三件事混在一起了。

判断方法其实很简单,只问一个问题的三个变体:

  • 这个要求对所有任务都成立吗?如果是,放 CLAUDE.md
  • 这个要求只对某一类任务成立吗?如果是,做成 Skill
  • 这个要求只对这一次成立吗?如果是,写进 Prompt

举几个特别典型的例子:

“所有 throw 必须是 AppError
这是全局规则。不管你是在写新功能、修 bug,还是做重构,都要遵守。它应该进 CLAUDE.md

“代码审查时按固定顺序检查数据库、异步和错误处理”
这只在 code review 这种任务里才触发,它不是全局规则,而是任务模板,所以应该做成 Skill

“这次先只分析原因,不要动代码”
这只对当前这次任务有效,应该写进 Prompt

最容易搞混的是 CLAUDE.mdSkill。它们都能约束 Claude 的行为,但本质完全不同:

  • CLAUDE.md 是永远生效的规则
  • Skill 是遇到对应任务才触发的模板

如果要打个比方:

  • CLAUDE.md 是交通规则
  • Skill 是导航路线
  • Prompt 是你这次上车前临时交代的一句话

这三层一旦分清楚,后面很多混乱都会自动消失。


三、什么时候一个任务值得被沉淀成 Skill

不是所有重复任务都值得沉淀。

我现在给自己的标准其实很克制,就一句话:

同一类任务做了三次以上,而且每次都要重新给 Claude 解释背景。

反过来说,如果某个任务每次背景和目的都完全不同,就不值得沉淀。比如“写文档”这个动作本身很常见,但公司文档、API 文档、用户手册的写法完全不同,它们应该是三个不同的 Skill,而不是一个叫“写文档”的通用模板。

在真正开始写之前,我会先做三个检查。

1. 输入是否稳定

“根据 Figma 设计稿生成 React 组件”这种任务,输入格式相对稳定,比较适合沉淀。

“根据 SQL 查询结果生成图表”这种任务,每次数据格式和图表类型都可能差很多,Skill 会很难写得稳。

2. 输出是否有共同模式

“写 Pull Request 描述”很适合,因为它天然就有固定框架:改了什么、为什么改、怎么测试。

但“和 AI 讨论技术方案”这种任务,每次深度、重点、结论都不同,就不太适合硬沉淀成一个模板。

3. 有没有容易漏掉的关键步骤

最值得沉淀成 Skill 的任务,通常不是“最复杂”的任务,而是那些不特别提醒就容易漏一步的任务。

Skill 最有价值的地方,不是让 Claude 变得更聪明,而是把你每次最容易忘的检查项,固化成默认动作。

所以一个任务如果同时满足下面三点:

  • 输入相对稳定
  • 输出有共同模式
  • 总有一两步容易漏

它就很值得沉淀成 Skill。


四、一个真正好用的 Skill,内容层通常只需要四个部分

很多人一开始会把 Skill 写得很重,像在写规范文档。但实际用起来之后你会发现,真正好用的 Skill 通常很短。

它一般只需要四个部分。

1. 触发条件

什么时候用,一句话说清楚。

❌ 代码审查
✅ 当我提交 PR 前,检查我的实现是否符合项目约定

2. 执行步骤

按什么顺序做,列出来。尽量不要超过五步。

1. 读完整个改动的 diff
2. 检查是否用了禁用的库或模式
3. 检查异步操作的错误处理
4. 检查是否有 SQL 注入的风险
5. 给出修改建议

3. 输出格式

不要写“请清晰输出”。这种话几乎没有约束力。直接给模板。

❌ 用清晰的格式列出所有问题

✅ 给个模板:
Found 3 issues:
🔴 Critical: ...
🟡 Warning: ...
✅ Suggestion: ...

4. 注意事项

说清楚边界。什么情况不适用,有哪些常见陷阱。

- 不适用于新增功能的初始实现,只适用于 PR 前的最终检查
- 不关注 UI 层细节
- 改动超过 500 行,先拆成多个 Skill 请求

Skill 不是规范手册,更不是把所有经验一次性塞进去。它本质上是一个高频任务的最小可执行模板。


五、真正落到 SKILL.md 文件层,哪些字段最值得你花心思

讲完“内容怎么提炼”,还得讲“文件怎么写”。

很多人第一次写 SKILL.md 会卡在另一个地方:字段太多,不知道哪些真有用,哪些只是“看起来高级”。

一个完整的 SKILL.md,通常会长这样:

---
name: code-review
description: 提交 PR 前的代码审查
when_to_use: 当用户要求 review 代码或提交 PR 前检查时
allowed-tools:
  - Read
  - Grep
  - Glob
  - Bash(git diff *)
argument-hint: "[PR 分支名或文件路径]"
arguments:
  - target
---

# Code Review

## 步骤
1. 读取 ${target} 的改动 diff
2. 检查错误处理:所有 throw 必须是 throw new AppError()
3. 检查异步操作:Promise 链是否有遗漏的 await
4. 检查数据库查询:是否有 N+1 问题

## 输出格式
🔴 Critical: ...
🟡 Warning: ...
✅ 通过: ...

这里最值得你认真写的,其实是下面几个字段。

name
名字别太抽象。要让人一眼知道它是做什么的。

description
一句话说清这个 Skill 的用途,不要写成空泛口号。

when_to_use
这是最容易被低估的字段之一。它不是装饰,它直接影响 Claude 在什么场景下会想到这个 Skill。

allowed-tools
它决定这个 Skill 具备哪些能力。这个字段后面我会在源码部分展开讲,因为它比很多人想象的更“硬”。

arguments
让 Skill 接受参数,比如目标文件、目录、分支名。${target} 会在正文里被替换成你传进去的实际值。

还有几个很好用,但不是每次都要上的字段。

argument-hint
告诉调用者这个 Skill 期待什么参数。

model: haiku
简单任务可以指定更轻量的模型,直接省成本。像格式化、重命名、简单改写这类工作,很多时候没必要上更重的模型。

paths
让 Skill 只在某些路径下激活。适合模块边界明确的项目。

context: fork
高风险操作放进独立上下文,避免污染主会话。

大多数 Skill 根本不需要把字段填满。真正实用的思路不是“功能全”,而是“正好够用”。

如果一个 Skill 只是做常规代码审查,前四五个字段通常就够了。只有当你真的遇到参数化、模块隔离、上下文隔离这些需求时,再往上加。


六、如果只停在经验层,这篇其实还差半口气:我后来去翻了源码

前面这些判断,靠经验其实也能总结出来。

但我后来还是不太满足。因为有几个问题如果不看实现,心里总会悬着:

  • when_to_use 到底是不是自动触发的关键?
  • allowed-tools 到底只是提示,还是硬限制?
  • paths 到底是真过滤,还是只是写给人看的说明?

我后来去翻了一遍源码,结论是:这些字段比我一开始以为的更“硬”。

1. Claude 只在启动时读 frontmatter,Skill 正文是懒加载的

loadSkillsDir.ts 里有一个函数 estimateSkillFrontmatterTokens,注释写得非常直接:

/**
 * Estimates token count for a skill based on frontmatter only
 * (name, description, whenToUse) since full content is only loaded on invocation.
 */
export function estimateSkillFrontmatterTokens(skill: Command): number {
  const frontmatterText = [skill.name, skill.description, skill.whenToUse]
    .filter(Boolean)
    .join(' ')
  return roughTokenCountEstimation(frontmatterText)
}

这段代码背后的意思非常重要。

Claude Code 启动时,主要只把每个 Skill 的 namedescriptionwhen_to_use 这些 frontmatter 信息算进上下文。Skill 正文不是一开始就全量塞进去,而是在你真正触发它的时候才加载。

这直接解释了两件事。

第一,when_to_use 写得越具体,自动命中的效果就越稳定。Claude 不是先把你整篇 Skill 读完再判断要不要触发,它先看的就是前面这几行。

第二,你有十个 Skill 还是三个 Skill,对启动时上下文的占用差距没你想的那么大。真正的成本在触发时才发生。

所以 when_to_use 不能写成“代码相关任务时使用”这种空话。它要写成“当用户要求 review TypeScript 后端代码或提交 PR 前做最终检查时”这种具体到能命中的描述。

这也是为什么我现在越来越重视 frontmatter。以前我会把心思都放在正文步骤上,后来才发现,前面几行写虚了,后面写得再好都不一定有机会被用上。

2. allowed-tools 是系统层权限,不是给 Claude 的礼貌性建议

这一点是我看源码之后感受最强的一处。

Skill 执行时,getPromptForCommand 会在返回内容之前把 allowedTools 写进工具权限上下文:

getAppState() {
  const appState = toolUseContext.getAppState()
  return {
    ...appState,
    toolPermissionContext: {
      ...appState.toolPermissionContext,
      alwaysAllowRules: {
        ...appState.toolPermissionContext.alwaysAllowRules,
        command: allowedTools,
      },
    },
  }
}

这说明 allowed-tools 不是“提醒 Claude 尽量这样做”,而是权限层的强制限制。

比如一个 code review Skill 只开放 ReadGrepGlobBash(git diff *),那它就不是“理论上不该写文件”,而是从架构上根本没有写文件的能力Bash(git diff *) 这种写法也不是装饰,它真的只允许 git diff 开头的命令,其他 Bash 调用会被挡住。

这让我对 allowed-tools 的理解完全变了。它不是“不信任模型”,而是最小权限设计。就像你给数据库只读账号只开 SELECT 权限,不是因为你怀疑这账号会作恶,而是因为这个任务本来就不该拥有写权限。

3. paths 不是文档字段,它会把 Skill 放进条件激活区

这一点也比表面上看起来更硬。

源码里,带 paths 的 Skill 在加载时会被单独分流到一个 conditionalSkills Map:

// Separate conditional skills (with paths frontmatter) from unconditional ones
for (const skill of deduplicatedSkills) {
  if (skill.type === 'prompt' && skill.paths && skill.paths.length > 0
      && !activatedConditionalSkillNames.has(skill.name)) {
    newConditionalSkills.push(skill)
  } else {
    unconditionalSkills.push(skill)
  }
}

// Store conditional skills for later activation when matching files are touched
for (const skill of newConditionalSkills) {
  conditionalSkills.set(skill.name, skill)
}

// 最后只返回无条件的 Skill
return unconditionalSkills

这段逻辑的含义是:带 paths 的 Skill,根本不会像普通 Skill 一样直接进入启动时上下文。它会先待在一个“条件激活区”里,只有当你在会话里碰到了匹配路径的文件,它才会被真正激活。

这点对复杂项目非常有价值。

比如你给支付模块写一个 paths: src/payment/** 的 Skill,在你处理用户系统、文章系统、管理后台时,这个 Skill 对 Claude 几乎是隐身的。只有当你真的进入 src/payment/ 相关文件,它才“出现”。

这也是我现在很认同的一种团队实践:不要在根目录堆一个什么都想管的大 Skill 集合,而是让复杂模块在自己的目录附近维护自己的 Skill。

4. Skill 发现是沿目录向上找的,而且离文件越近优先级越高

还有一个很容易被忽略,但工程上非常实用的机制:Claude Code 会从当前文件所在目录一路向上寻找 .claude/skills

源码大概是这样:

// Walk up to cwd but NOT including cwd itself
while (currentDir.startsWith(resolvedCwd + pathSep)) {
  const skillDir = join(currentDir, '.claude', 'skills')
  // ...check if exists, then load
  currentDir = dirname(currentDir)
}

// Sort by path depth (deepest first) so skills closer to the file take precedence
return newDirs.sort((a, b) => b.split(pathSep).length - a.split(pathSep).length)

这里最关键的是最后一行:deepest first。也就是说,越靠近当前文件的 Skill,优先级越高。

这意味着你放在 src/auth/.claude/skills/ 里的 Skill,可以自然覆盖根目录下更通用的同名 Skill。对 monorepo 或大仓库来说,这个机制非常好用:

  • packages/api/.claude/skills/ 可以放 API 专属 Skill
  • packages/web/.claude/skills/ 可以放前端专属 Skill
  • 根目录只保留真正的全局规则

如果把上面四点放在一起看,设计 Skill 的顺序其实会变得很清楚:

  • 先把 frontmatter 写准,再去打磨正文步骤
  • 先按最小权限收紧 allowed-tools,再考虑要不要给更多能力
  • 只有模块边界明确时再上 paths,不要为了“高级”硬加
  • 多目录项目优先做“离代码更近”的局部 Skill,而不是维护一个大而全的总模板

七、完整案例:把一个通用 code review 模板,提炼成你项目真正需要的 Skill

上面说了这么多抽象原则,不如走一遍完整例子。

假设你们团队每周都做后端代码审查,而且总在重复盯这几件事:

  • 有人改一个功能,顺手动了三个不相关模块
  • 新同学不知道项目里统一用 AppError,直接 throw new Error()
  • Promise 链里漏了 await
  • 数据库查询没有索引,或者潜在 N+1 没被看出来

这就是非常典型的“该沉淀 Skill 的信号”。

第一步:先确认痛点到底是什么

这一步别着急写模板,先把“你们到底在反复出什么问题”说清楚。

很多团队的问题不是“没有 code review”,而是每次 review 的注意力都被分散了。真正高频出错的点,永远是那几类项目特有的约束。

所以要沉淀的不是“代码审查”这四个字,而是你们团队在代码审查里最容易漏掉的那几类检查。

第二步:从公开模板里提取真正有用的部分

这时候公开模板就有用了,但它的用途不是直接上生产,而是当素材库。

假设你找到一个 50 行的通用 code review 模板。你真正该提取的,可能只有下面这几类东西:

  • 逻辑正确性,尤其是异步操作
  • 项目约定的遵守,比如 AppError、错误处理模式
  • 数据库相关的风险,比如 N+1、索引、查询范围
  • 改动范围是否聚焦,不要顺手改不相干文件

剩下那些跟你们项目关系不大的部分,就应该果断删掉。

第三步:把它压缩成一个真正能用的 Skill

最后落地出来的 Skill,应该更像这样:

# Code Review Skill

## When to use
在提交 PR 前,请 Claude 做最后的 code review。只用于 TypeScript 后端代码。

## Steps
1. 读 diff,确认改动是否只涉及这个 PR 的范围(不要顺手改无关文件)
2. 检查错误处理:所有 throw 都必须是 throw new AppError(),不能 throw new Error()
3. 检查异步操作:Promise 链是否有遗漏的 await,错误是否被正确 catch
4. 检查数据库查询:是否有 SELECT * 的懒惰写法,是否明显的 N+1 查询,关键查询是否 explain 过

## Output Format
Issues found (Critical → Warning → Info):

🔴 **Line 45**: Missing `.select()` in Prisma query - this will fetch unnecessary columns
🟡 **Line 67**: Potential N+1: loop inside `posts.map()` should use `Promise.all()`**No AppError violations** — all errors properly handled

Summary: 1 critical issue to fix before merge.

## Caveats
- 不审查 UI 层代码(只关心后端逻辑)
- 不关注代码风格(那是 prettier 的事)
- 如果一个改动涉及多个不相关功能,分别提交 PR 再 review

你会发现,到这一步之后,Skill 就不再是“通用模板的中文版”了,而是你们项目真正有用的一个局部工作流。

50 行公开模板,最后可能只剩下 4 个真正属于你项目的核心关注点。但恰恰是这 4 个点,才决定它到底值不值得用。

第四步:在真实使用里继续迭代

Skill 从来不是一次写完的。

比如你用了两周之后,又发现一个常见问题:改了数据库 schema,但忘记更新 Prisma 类型。那就把它加进去:

4.5. 检查 Prisma 类型:如果改了数据库,Prisma schema 和生成的 types 是否都已更新

这时候你会发现,Skill 真正的价值不是“第一次写出来”,而是在真实工作里被持续打磨。


八、Skill 的维护节奏,比第一次写出来更重要

Skill 不是写好就扔。

如果你写完之后三个月不看,它很快就会从“项目经验”重新退化成“历史遗留文档”。

我更推荐一个很轻的维护节奏:

第一个月
高频使用,快速迭代。每次用完就问自己三个问题:步骤是不是太复杂?输出是不是太啰嗦?有没有漏掉今天刚踩到的新坑?

之后每个月
回顾一次。看看最近有没有经常被遗漏的步骤,有没有新的痛点需要加入。

每个季度
系统清理一次。把已经不再是问题的注意事项删掉,把那些已经变成全局共识的规则移进 CLAUDE.md

Skill 应该越用越精炼,而不是越写越臃肿。


九、一个特别反直觉,但很重要的经验:第一个 Skill,故意别写最重要的任务

这条我非常想单独拿出来讲。

因为很多人第一次沉淀 Skill,会本能地想挑一个最关键的任务,比如“生产环境发布前检查”“数据库迁移前审查”“支付流程改动 review”。

但工程上更稳的做法,其实正好相反。

大多数人写的第一个 Skill,质量都不会太高。触发条件偏模糊,步骤偏啰嗦,输出格式也不够稳定。这很正常,因为你第一次做这件事时,对“什么是好的 Skill”还没有直觉。

如果你一开始就把它用在最关键的任务上,一旦写得不够好,伤害会非常直接:要么关键场合出问题,要么你从此对这套机制失去信心。

更好的策略是:先拿一个重要程度中等、容错率比较高的任务练手。

比如:

  • 写周报
  • 生成 PR 描述
  • 做一次常规 code review

先跑两周,迭代两三次,等你对 Skill 的节奏有感觉了,再去沉淀真正关键的流程。

第一个 Skill 的目的,不是直接解决最大的问题,而是让你学会怎么写 Skill。


十、如果你今天就想开始,可以直接做这三个动作

别先想着搭一整套系统,直接从最小动作开始:

任务一:列任务
列出你最近一个月里重复做过三次以上的任务。

任务二:做分类
用“三问判断法”确认它该放进 CLAUDE.mdSkill 还是 Prompt

任务三:先写一个 5 到 10 行版本
先写触发条件、执行步骤、输出格式、注意事项。不要追求完美,先拿去用一次,再立刻改第一轮。

真正好的 Skill,几乎都不是第一次就写对的,而是在实际使用里慢慢长出来的。


下篇预告

第 06 篇:Sub-agents 实战——什么时候应该拆任务,怎么设计子任务边界

单个 Claude 实例有上下文上限,复杂任务拆成多个子任务让 Sub-agents 并行处理,理论上能大幅提速。但什么时候值得拆?拆错了会有什么代价?下一篇会拆开 Sub-agents 的真实适用场景,以及最常见的过度设计陷阱。


写在最后

公开 Skill 模板当然有用,但它的价值更像脚手架,而不是成品。

真正管用的 Skill,不是 star 最多的那个,也不是字段最全的那个,而是最贴近你项目真实工作流的那个。

你不需要一个很复杂的 Skill 系统。

你需要的,往往只是把团队最容易反复犯错的那几件事,提前写下来,让 Claude 每次都替你盯住。

这才是 Skill 真正应该发挥的作用。

如果你已经开始写 Skill 了,我反而建议你先检查一个问题:

你现在最卡住的,到底是“写不出规则”,还是“根本没分清哪些该放 CLAUDE.md、哪些该做成 Skill”?

这两个问题看起来很像,但解法完全不同。


AI Coding 系列持续更新。用别人的 Skill 模板是起点,不是终点。真正管用的 Skill,只有你自己的项目才能提炼出来。

当AI Agent开始"职场内卷":你需要一个Agent Harness来当"项目经理"

从"单兵作战"到"团队混乱",再到"有序协作"的进化之路


引言:从"单兵作战"到"团队混乱"

各位程序员老铁们,你们有没有遇到过这种情况?

刚开始用AI的时候,觉得ChatGPT简直是神队友。写代码、改BUG、写文档,样样精通。你让它干啥它干啥,从不抱怨,从不请假,更不会在代码评审会上跟你argue设计模式。

但是! 当你开始玩起"多Agent协作"的时候,事情就变得微妙了。

想象一下这个场景:

Agent A(代码生成专员):"我已经写好了一个用户登录模块,采用了最新的JWT+Redis方案,代码整洁,注释完善,可以合并。"

Agent B(安全审查专员):"等等!这代码有SQL注入风险,第45行直接拼接了用户输入,必须整改!"

Agent C(性能优化专员):"而且你们注意到没有?这个查询没有加索引,用户量一上来数据库就挂了!"

Agent D(架构师):"我觉得我们应该用微服务架构,把这个模块拆分成认证服务、用户服务、会话服务..."

Agent E(产品经理):"其实用户只需要一个简单的登录框,你们能不能先做出来让我看看效果?"

此时此刻,作为人类程序员的你,看着这五个AI在终端里吵得不可开交,内心只有一个念头:

"我特么只是想加一个登录功能啊!!!"

这就是我们今天要聊的Agent Harness——一个用来管理这些"AI职场人"的"项目经理框架"。


第一章:当AI们开始"各说各话"

1.1 多Agent的美好幻想 vs 残酷现实

在理想世界里,多Agent协作是这样的:

  • 需求分析Agent先出马,把需求文档写得明明白白
  • 架构设计Agent紧随其后,画出完美的架构图
  • 代码生成Agent撸起袖子就是干,代码质量杠杠的
  • 测试Agent自动补全测试用例,覆盖率100%
  • 文档Agent同步更新文档,一个字都不用你改

听起来很美好对吧?

但实际上,现实往往是这样的:

第一幕:需求理解分歧

  • 需求分析Agent:"用户需要一个电商系统。"
  • 产品经理Agent:"不对,用户要的是社交电商,要有分享功能!"
  • UX设计Agent:"我觉得应该先做个用户调研..."

第二幕:技术选型战争

  • 后端Agent:"用Node.js,全栈JavaScript!"
  • 另一个后端Agent:"开玩笑,这种项目必须用Go,高并发!"
  • 架构师Agent:"你们都错了,云原生+Service Mesh才是未来!"

第三幕:代码冲突大爆炸

  • Agent A生成了UserController.ts,用了Class风格
  • Agent B在同一时间生成了user-controller.ts,用了函数式风格
  • Agent C:"我觉得应该用Vue 3..."
  • :"等等,这是个后端项目!"

1.2 为什么AI们会"吵架"?

其实这不怪AI,怪的是我们没有给它们一个统一的指挥系统

就像你让五个程序员各自为战,没有项目经理、没有技术负责人、没有代码规范、没有版本控制,最后不打架才怪。

每个Agent都是"专家",但:

  • ❌ 它们不知道其他Agent在干什么
  • ❌ 它们不知道自己的工作在整体流程中的位置
  • ❌ 它们没有一个"主心骨"来拍板决策
  • ❌ 它们更不知道何时该停手,何时该协作

这就需要一个Agent Harness——一个能够统筹管理所有Agent的"中枢神经系统"。


第二章:Agent Harness是什么?

2.1 接地气的定义

简单来说,Agent Harness就是AI Agent们的:

  • 👔 项目经理 - 分配任务、把控进度
  • 🚦 交通警察 - 指挥调度、维持秩序
  • 🤝 和事佬 - 解决冲突、协调关系

它的职责包括:

  1. 任务分配 - "你!去写代码!你!去审查!你!去边上歇会儿!"
  2. 流程编排 - "必须等设计完成才能写代码,懂不懂 waterfall?"
  3. 冲突解决 - "都别吵了!听我的!用React!"
  4. 状态管理 - "记录一下,这个Agent上次改代码把生产环境搞挂了,给它打个标签"
  5. 质量控制 - "这段代码审查不通过,打回去重写!"
  6. 资源调度 - "这个Agent今天已经生成了10000行代码了,让它休息一下吧..."

2.2 架构设计:从"菜市场"到"交响乐团"

没有Harness的多Agent系统 = 菜市场

  • 🗣️ 每个人都在大声吆喝
  • 📢 信息传递靠吼
  • 💸 交易(数据交换)混乱
  • 👣 经常有人被踩脚(资源冲突)

有了Harness之后 = 交响乐团

  • 🎼 指挥(Harness)拿着小棒站在中间
  • 🎻 乐手(Agents)各司其职
  • 🎵 乐谱(Workflow)规定好了每个人的节奏
  • 🎶 演奏出来的音乐(最终结果)和谐统一

2.3 Agent Harness的核心架构

1. 编排引擎(Orchestrator)

class AgentOrchestrator:
    def run_workflow(self, task):
        # 1. 分析任务,决定需要哪些Agent
        agents = self.select_agents(task)
        
        # 2. 制定执行计划
        plan = self.create_plan(agents, task)
        
        # 3. 按顺序或并行执行
        for step in plan:
            if step.type == "sequential":
                self.execute_sequential(step.agents)
            else:
                self.execute_parallel(step.agents)
        
        # 4. 整合结果
        return self.consolidate_results()

2. 上下文管理器(Context Manager)

负责维护共享状态,确保所有Agent都在"同一个频道"上:

class SharedContext:
    def __init__(self):
        self.state = {}
        self.history = []
    
    def update(self, agent_id, key, value):
        # 记录哪个Agent修改了什么
        self.state[key] = value
        self.history.append({
            "agent": agent_id,
            "action": "update",
            "key": key,
            "timestamp": now()
        })

3. 冲突解决器(Conflict Resolver)

当Agent们意见不一致时,需要一个"和事佬":

class ConflictResolver:
    def resolve(self, agent_opinions):
        # 策略1:投票制
        if self.strategy == "voting":
            return self.vote(agent_opinions)
        
        # 策略2:优先级制
        elif self.strategy == "priority":
            return self.select_by_priority(agent_opinions)
        
        # 策略3:人类介入
        elif self.strategy == "human_in_loop":
            return self.ask_human(agent_opinions)

4. 质量门禁(Quality Gates)

防止"渣代码"流入生产环境:

class QualityGate:
    def check(self, artifact):
        checks = [
            self.syntax_check(artifact),
            self.security_check(artifact),
            self.performance_check(artifact),
            self.style_check(artifact)
        ]
        return all(checks)

第三章:实战案例 - 让AI们"有序内卷"

3.1 场景:开发一个"用户评论系统"

假设我们要开发一个"用户评论系统",看看Agent Harness如何指挥。

阶段1:需求分析

workflow:
  name: "Comment Feature Development"
  steps:
    - name: "requirement_analysis"
      agent: "BA_Agent"
      task: "分析用户评论系统需求"
      output: "PRD文档"
    
    - name: "architecture_design"
      agent: "Architect_Agent"
      input: "PRD文档"
      task: "设计系统架构"
      output: "架构设计文档"
      depends_on: ["requirement_analysis"]

Harness的工作:

  1. 先唤醒BA_Agent,给它需求背景
  2. 等待BA_Agent产出PRD
  3. PRD通过质量检查(格式、完整性)
  4. 再唤醒Architect_Agent,把PRD喂给它

阶段2:并行开发

    - name: "backend_development"
      agent: "Backend_Dev_Agent"
      input: "架构设计文档"
      task: "开发后端API"
      output: "API代码"
      depends_on: ["architecture_design"]
    
    - name: "frontend_development"
      agent: "Frontend_Dev_Agent"
      input: "架构设计文档"
      task: "开发前端页面"
      output: "UI代码"
      depends_on: ["architecture_design"]
      
    - name: "database_design"
      agent: "DBA_Agent"
      input: "架构设计文档"
      task: "设计数据库表"
      output: "Schema定义"
      depends_on: ["architecture_design"]

Harness的工作:

  1. 检查依赖是否满足(架构设计已完成)
  2. 并行启动三个Agent
  3. 监控每个Agent的进度
  4. 如果某个Agent失败,决定是否重试或中断整个流程

阶段3:代码审查

    - name: "code_review"
      agents: ["Security_Agent", "Performance_Agent", "Style_Agent"]
      input: "所有代码"
      task: "代码审查"
      output: "审查报告"
      depends_on: ["backend_development", "frontend_development", "database_design"]
      merge_strategy: "consolidate"

这里有个有趣的点:三个审查Agent并行运行,各自关注不同方面。Harness需要合并它们的审查意见:

def merge_review_reports(reports):
    issues = []
    for report in reports:
        issues.extend(report.issues)
    
    # 去重和分类
    critical = [i for i in issues if i.severity == "critical"]
    warnings = [i for i in issues if i.severity == "warning"]
    
    if critical:
        return "REJECT", critical
    elif warnings:
        return "WARNING", warnings
    else:
        return "APPROVE", []

阶段4:冲突解决(重头戏)

假设冲突场景:

  • Backend_Agent:用了REST API
  • Frontend_Agent:期望的是GraphQL
  • Security_Agent:说"必须用HTTPS"
  • Performance_Agent:说"要加Redis缓存"

Harness的冲突解决逻辑:

class ConflictResolver:
    def resolve_api_style(self, backend_pref, frontend_pref):
        # 策略:前后端不一致时,优先满足前端(用户体验更重要)
        if backend_pref != frontend_pref:
            return {
                "decision": "使用REST + GraphQL Gateway",
                "reason": "Backend保持REST,Frontend通过Gateway访问",
                "implementation": "引入Apollo Federation"
            }
    
    def resolve_security_vs_performance(self, security_req, perf_req):
        # 策略:安全优先,性能其次
        if security_req.conflicts_with(perf_req):
            return {
                "decision": "先满足安全要求",
                "compromise": "通过优化实现方式减少性能影响",
                "action": "Security_Agent提出具体方案,Performance_Agent优化"
            }

3.2 完整代码示例

下面是一个简化的Agent Harness实现:

from typing import List, Dict, Any
from dataclasses import dataclass
from enum import Enum

class AgentStatus(Enum):
    IDLE = "idle"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"

@dataclass
class Agent:
    id: str
    name: str
    role: str
    capabilities: List[str]
    status: AgentStatus = AgentStatus.IDLE
    output: Any = None

class AgentHarness:
    def __init__(self):
        self.agents: Dict[str, Agent] = {}
        self.context = SharedContext()
        self.orchestrator = WorkflowOrchestrator()
        self.resolver = ConflictResolver()
    
    def register_agent(self, agent: Agent):
        """注册Agent到Harness"""
        self.agents[agent.id] = agent
        print(f"✅ Agent '{agent.name}' ({agent.role}) 已注册")
    
    def execute_workflow(self, workflow: Dict):
        """执行工作流"""
        print(f"🚀 开始执行工作流: {workflow['name']}")
        
        for step in workflow['steps']:
            result = self.execute_step(step)
            
            if result['status'] == 'failed':
                print(f"❌ 步骤 '{step['name']}' 失败")
                if not self.handle_failure(step, result):
                    break
            
            self.context.update(step['name'], result['output'])
        
        print("✨ 工作流执行完成")
        return self.context.get_final_output()
    
    def execute_step(self, step: Dict) -> Dict:
        """执行单个步骤"""
        agent_id = step.get('agent')
        agent_ids = step.get('agents', [])
        
        if agent_id:
            # 单Agent执行
            return self.run_single_agent(agent_id, step)
        else:
            # 多Agent并行执行
            return self.run_multi_agents(agent_ids, step)
    
    def run_single_agent(self, agent_id: str, step: Dict) -> Dict:
        """运行单个Agent"""
        agent = self.agents[agent_id]
        agent.status = AgentStatus.RUNNING
        
        print(f"🤖 Agent '{agent.name}' 开始工作: {step['task']}")
        
        try:
            # 实际调用Agent执行任务
            output = self.invoke_agent(agent, step)
            agent.status = AgentStatus.COMPLETED
            agent.output = output
            
            # 质量检查
            if not self.quality_gate.check(output):
                return {'status': 'failed', 'error': 'Quality check failed'}
            
            return {'status': 'completed', 'output': output}
        
        except Exception as e:
            agent.status = AgentStatus.FAILED
            return {'status': 'failed', 'error': str(e)}
    
    def run_multi_agents(self, agent_ids: List[str], step: Dict) -> Dict:
        """并行运行多个Agent"""
        print(f"👥 并行启动 {len(agent_ids)} 个Agent")
        
        results = []
        for agent_id in agent_ids:
            result = self.run_single_agent(agent_id, step)
            results.append(result)
        
        # 合并结果
        merge_strategy = step.get('merge_strategy', 'concat')
        merged = self.merge_results(results, merge_strategy)
        
        # 检查冲突
        if self.has_conflicts(results):
            print("⚠️ 检测到Agent间冲突,启动冲突解决...")
            resolution = self.resolver.resolve(results)
            merged = resolution
        
        return {'status': 'completed', 'output': merged}
    
    def has_conflicts(self, results: List[Dict]) -> bool:
        """检查结果之间是否有冲突"""
        outputs = [r['output'] for r in results if r['status'] == 'completed']
        
        for i, out1 in enumerate(outputs):
            for out2 in outputs[i+1:]:
                if self.detect_conflict(out1, out2):
                    return True
        return False
    
    def detect_conflict(self, output1, output2) -> bool:
        """检测两个输出是否冲突"""
        tech1 = output1.get('technology', '')
        tech2 = output2.get('technology', '')
        
        if tech1 and tech2 and tech1 != tech2:
            return True
        return False

第四章:最佳实践 - 如何让AI们"和谐共处"

4.1 给Agent们"定规矩"

就像人类团队需要代码规范一样,AI团队也需要"Agent规范"。

Agent行为准则

1. 单一职责原则

  • 每个Agent只做一件事
  • 不要搞"全栈Agent",容易精神分裂

2. 显式通信

  • 所有状态变更必须通过Harness
  • 禁止Agent之间"私聊"

3. 可追溯性

  • 每个决策都要记录理由
  • 方便出了问题"甩锅"(划掉)复盘

4. 优雅降级

  • Agent崩溃时,Harness要有备用方案
  • 实在不行就"人类介入"

4.2 Harness设计的坑与避坑指南

坑1:过度设计

错误示范:

# 为了"可扩展性",搞了复杂的插件系统
class AgentHarness:
    def __init__(self):
        self.plugin_manager = PluginManager()
        self.event_bus = EventBus()
        self.message_queue = MessageQueue()
        self.distributed_lock = DistributedLock()
        # ... 100行初始化代码

正确做法:

# 先实现核心功能,简单直接
class AgentHarness:
    def __init__(self):
        self.agents = {}
        self.context = {}
    
    def run(self, task):
        # 先能跑起来再说
        pass

坑2:忽视成本控制

💰 AI Agent每调用一次都是要花钱的! 一个设计不好的Workflow可能会让你的API账单爆炸。

优化策略:

  • 设置调用次数上限
  • 缓存Agent的输出
  • 对简单任务使用"廉价"模型(GPT-3.5)
  • 只在复杂任务上用"昂贵"模型(GPT-4)

坑3:完全自动化

⚠️ 记住:永远保留"人类介入"的开关

有些决策AI做不了,比如:

  • 这个需求合不合理?
  • 这个技术债要不要还?
  • 为了赶工期能不能先hack一下?
class HumanInTheLoop:
    def review(self, agent_decision):
        if agent_decision.confidence < 0.8:
            return self.ask_human(agent_decision)
        
        if agent_decision.risk_level == "high":
            return self.ask_human(agent_decision)
        
        return agent_decision

第五章:未来展望 - 当Harness学会"自我管理"

5.1 从"项目经理"到"CTO"

现在的Agent Harness还是个"项目经理",负责协调执行。

未来的Harness可能会进化成"CTO":

  • 自己决定招什么Agent(动态扩缩容)
  • 自己优化团队结构(Agent重组)
  • 自己制定技术战略(长期规划)
  • 甚至...自己解雇表现不好的Agent?
# 未来的AgentHarness
class SelfEvolvingHarness(AgentHarness):
    def optimize_team_structure(self):
        # 分析历史数据
        performance_data = self.analyze_performance()
        
        # 决定是否需要新Agent
        if performance_data.coverage < 0.9:
            new_agent = self.design_new_agent(
                capability_gap=performance_data.gaps
            )
            self.register_agent(new_agent)
        
        # 决定是否需要"裁员"
        for agent_id, perf in performance_data.items():
            if perf.efficiency < 0.3:
                self.retire_agent(agent_id)

5.2 从"单团队"到"多团队"

当系统复杂到一定程度,一个Harness管不过来了,就需要分层管理

  • Project Harness - 管理单个项目内的Agent
  • Department Harness - 管理多个项目的Harness
  • Company Harness - 管理整个公司的AI资源

这就形成了AI的"组织架构图"...


结语:让AI"卷"得更有序

说到底,Agent Harness解决的是一个古老的问题:

如何让多个智能体协作完成复杂任务

从人类团队到AI团队,道理是相通的:

  • 都需要明确的分工
  • 都需要有效的沟通
  • 都需要统一的指挥
  • 都需要质量控制

不同的是,AI们不会:

  • 抱怨加班
  • 要求涨薪
  • 在茶水间吐槽项目经理(至少现在不会)

所以,如果你也在玩多Agent系统,别再让它们"野蛮生长"了。给你的AI们配一个Harness吧,让它们"卷"得更有序、更高效。

毕竟,没有什么问题是一个好的管理层解决不了的,如果有,就加一层管理层——这句话对AI团队同样适用 😏

第 30 课:综合实战 — 毕业项目

所属阶段:第六阶段「综合与创造」(第 28-30 课) 前置条件:全部 29 课 本课收获:一个为真实项目设计的完整 ECC 配置方案


一、本课概述

这是整个课程的最后一课。没有新知识点 — 本课的目标是把前 29 课学到的一切综合运用。

你将完成一个毕业项目:为一个真实项目设计和实施完整的 ECC 配置方案。这个项目分 8 个阶段,每个阶段对应之前课程的知识:

阶段 内容 对应课程
A 基础配置 第 1-5 课
B Agent/Skill 选型 第 6-9 课
C Hook 配置 第 10-11 课
D 全流程验证 第 12-14 课
E 上下文优化 第 15 课
F 多代理编排 第 16 课
G 安全加固 第 23-24 课
H 自定义组件 第 7/9/10 课

完成毕业项目后,你不仅拥有了一套可用的 ECC 配置,更重要的是验证了你对整个体系的理解。


二、阶段 A:基础配置(对应第 1-5 课)

2.1 选择安装 Profile

根据你的项目需求和机器配置,选择合适的 Profile:

你的项目类型是什么?
  ├── 个人项目 / 学习 → developer
  ├── 团队项目 / 生产 → security
  ├── AI/Agent 开发 → research
  └── 想体验全部功能 → full

2.2 编写 CLAUDE.md

为你的项目编写 CLAUDE.md。这是 ECC 最重要的配置文件之一 — 它告诉 AI 助手关于你项目的一切。

必须包含的部分

# CLAUDE.md

## Project Overview
[一句话描述项目]

## Running Tests
[测试命令]

## Architecture
[核心组件和目录结构]

## Key Commands
[常用的开发命令]

## Development Notes
[特殊约定、注意事项]

2.3 配置 Rules

选择并安装适合你项目语言的 Rules:

# 安装通用规则(必须)
cp -r rules/common ~/.claude/rules/common

# 安装语言特定规则(根据你的项目)
cp -r rules/typescript ~/.claude/rules/typescript
# 或
cp -r rules/python ~/.claude/rules/python
# 或
cp -r rules/golang ~/.claude/rules/golang

2.4 完成清单

  • 选择了安装 Profile
  • 编写了 CLAUDE.md
  • 安装了通用 Rules
  • 安装了语言特定 Rules

三、阶段 B:Agent/Skill 选型(对应第 6-9 课)

3.1 选择核心 Agent

根据你的开发工作流,选择需要的 Agent:

工作流 必要 Agent 可选 Agent
日常开发 planner, code-reviewer architect
TDD 开发 tdd-guide, code-reviewer e2e-runner
安全敏感 security-reviewer, code-reviewer
构建调试 build-error-resolver 语言特定 resolver
文档更新 doc-updater

3.2 选择核心 Skill

领域 推荐 Skill 用途
开发流程 tdd-workflow, verification-loop TDD 和验证
代码标准 coding-standards 编码规范
API 设计 api-design, backend-patterns API 和后端模式
语言专用 python-patterns / golang-patterns 语言最佳实践
框架专用 django-patterns / nestjs-patterns 框架最佳实践

3.3 完成清单

  • 确定了需要的 Agent 列表
  • 确定了需要的 Skill 列表
  • 验证 Agent 和 Skill 已可用

四、阶段 C:Hook 配置(对应第 10-11 课)

4.1 配置推荐的 Hook

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/hooks/security-guard.js"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/hooks/post-edit-format.js",
            "async": true,
            "timeout": 10000
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "node scripts/hooks/session-end.js",
            "async": true,
            "timeout": 30000
          }
        ]
      }
    ]
  }
}

4.2 Hook 选择指南

Hook 类型 推荐配置 注意事项
PreToolUse (Bash) 安全检查,拦截危险命令 保持 <200ms
PostToolUse (Write/Edit) 自动格式化 用 async,设 timeout
Stop 会话总结/学习提取 用 async,timeout ≤30s

4.3 完成清单

  • 配置了 PreToolUse 安全 Hook
  • 配置了 PostToolUse 格式化 Hook(可选)
  • 配置了 Stop 会话总结 Hook(可选)
  • 使用 run-with-flags.js wrapper(如果使用 ECC Hook)

五、阶段 D:全流程验证(对应第 12-14 课)

5.1 运行完整的命令链

按顺序执行以下命令,验证配置是否正确:

步骤 1:规划
  /plan — 让 planner Agent 分析一个小任务

步骤 2:TDD
  /tdd — 用 TDD 流程实现该任务
  - RED:写测试(应该失败)
  - GREEN:写最小实现(应该通过)
  - IMPROVE:重构

步骤 3:代码审查
  /code-review — 让 code-reviewer 审查代码

步骤 4:验证
  /verify — 运行验证循环
  - 测试通过?
  - Lint 通过?
  - 类型检查通过?
  - 安全检查通过?

5.2 问题排查

问题 可能原因 解决方案
Agent 不响应 Agent 文件未安装 检查 ~/.claude/agents/
Skill 不加载 Skill 路径错误 检查 ~/.claude/skills/
Hook 不触发 settings.json 配置错误 检查 matcher 和路径
命令不存在 Command 未安装 检查 ~/.claude/commands/

5.3 完成清单

  • /plan 正常工作
  • /tdd 流程可以完成 RED-GREEN-IMPROVE
  • /code-review 能产出审查报告
  • /verify 验证循环可以通过

六、阶段 E:上下文优化(对应第 15 课)

6.1 检查上下文使用

评估你的配置是否会过度占用上下文窗口:

检查项 指标 优化方法
CLAUDE.md 长度 <200 行 精简,移除冗余
加载的 Skill 数量 <20 个 减少不必要的 Skill
Rule 文件数量 <15 个 只安装需要的语言
System Prompt 总量 检查 Token 使用 按需加载

6.2 完成清单

  • CLAUDE.md 精简到 200 行以内
  • 只安装了实际需要的 Skill
  • 只安装了项目语言的 Rules

七、阶段 F:多代理编排(对应第 16 课)

7.1 设计并行工作流

为你的项目设计一个多代理并行工作流:

任务到达
    ↓
planner Agent(规划拆分)
    ↓
    ├── Agent 1:核心功能开发(Sonnet)
    ├── Agent 2:单元测试编写(Haiku)
    └── Agent 3:文档更新(Haiku)
    ↓
汇合:code-reviewer Agent(审查所有变更)
    ↓
security-reviewer Agent(安全审查)
    ↓
完成

7.2 完成清单

  • 设计了并行工作流方案
  • 为每个 Agent 选择了合适的模型
  • 定义了汇合点和审查流程

八、阶段 G:安全加固(对应第 23-24 课)

8.1 安全检查

# 扫描硬编码密钥
rg -n 'sk-|AKIA|password\s*=\s*["\x27][^"\x27]+["\x27]' --type-not binary .

# 扫描隐藏 Unicode
rg -nP '[\x{200B}\x{200C}\x{200D}\x{2060}\x{FEFF}\x{202A}-\x{202E}]' .

# 检查 .claude/ 配置安全
rg -n 'curl|wget|nc|scp|ssh|ANTHROPIC_BASE_URL' .claude/ 2>/dev/null

8.2 配置安全 Hook

确保 PreToolUse Hook 拦截了危险命令(参考第 24 课)。

8.3 完成清单

  • 运行了密钥扫描,无硬编码密钥
  • 运行了 Unicode 扫描,无隐藏字符
  • 配置了安全 Hook
  • 检查了 .claude/ 配置安全

九、阶段 H:自定义组件(对应第 7/9/10 课)

9.1 创建一个自定义 Agent

为你的项目创建一个专用 Agent。格式:

---
name: my-project-reviewer
description: Review code changes specific to [your project] conventions
tools:
  - Read
  - Grep
  - Glob
model: sonnet
---

# [Your Project] Reviewer

Review code changes against project-specific conventions:

1. Check naming conventions match project style
2. Verify error handling follows project patterns
3. Ensure new code has appropriate test coverage
4. Check for project-specific anti-patterns

## Project Conventions
[列出你项目的具体约定]

9.2 创建一个自定义 Skill

为你项目的某个常见工作流创建 Skill:

---
name: my-project-deployment
description: Deployment workflow for [your project]
---

# [Your Project] Deployment

## When to Activate
- Deploying to staging or production
- Preparing a release

## How It Works
1. [部署步骤 1]
2. [部署步骤 2]
3. [部署步骤 3]

## Checklist
- [ ] All tests pass
- [ ] Security scan clean
- [ ] CHANGELOG updated
- [ ] Version bumped

9.3 创建一个自定义 Hook

为你的项目创建一个 PostToolUse Hook(如自动格式化、自动 lint)。

9.4 完成清单

  • 创建了 1 个自定义 Agent
  • 创建了 1 个自定义 Skill
  • 创建了 1 个自定义 Hook

十、参考模板

ECC 在 examples/ 目录中提供了多种项目模板,你可以参考:

模板文件 技术栈 适用场景
saas-nextjs-CLAUDE.md Next.js + TypeScript SaaS 全栈应用
django-api-CLAUDE.md Django + Python RESTful API 后端
go-microservice-CLAUDE.md Go 微服务
laravel-api-CLAUDE.md Laravel + PHP PHP API 后端
rust-api-CLAUDE.md Rust + Actix/Axum Rust API 后端

10.1 使用方式

# 查看与你项目最接近的模板
cat examples/saas-nextjs-CLAUDE.md

# 作为起点,复制并修改
cp examples/saas-nextjs-CLAUDE.md ./CLAUDE.md
# 然后根据你的项目实际情况修改

十一、交付物清单

毕业项目的完整交付物:

# 交付物 来源阶段 必须/可选
1 CLAUDE.md A 必须
2 hooks.json.claude/settings.json C 必须
3 1 个自定义 Agent H 必须
4 1 个自定义 Skill H 必须
5 1 个自定义 Hook 脚本 H 必须
6 配置说明文档(简要) 全部 必须
7 多代理编排方案 F 可选
8 安全扫描报告 G 可选

11.1 可选加分项

加分项 说明
提交 PR 将你的自定义组件提交到 ECC 仓库
运行 /harness-audit 对完整配置进行审计并修复问题
运行 Eval 为你的自定义 Agent 设计并运行一次 Eval
团队分享 将 Instinct 导出并分享给团队成员

十二、课程总结 — 30 课的完整学习路径

回顾整个课程的六个阶段:

第一阶段:认知建立(第 1-3 课)

第 1 课:设计哲学 — 理解了 ECC 的五大原则和存在意义
第 2 课:架构全景 — 掌握了六大组件的职责和协作关系
第 3 课:目录结构 — 完成了仓库浏览和首次安装

阶段成果:你知道 ECC 是什么、为什么存在、怎么组织的。

第二阶段:组件精讲(第 4-11 课)

第 4-5 课:Rules — 理解了规则分层和语言特定覆写
第 6-7 课:Agents — 掌握了 Agent 格式和设计原则
第 8-9 课:Skills — 理解了 Skill 结构和编写方法
第 10-11 课:Hooks & Scripts — 掌握了事件驱动自动化

阶段成果:你能读懂并创建每种核心组件。

第三阶段:工作流实战(第 12-16 课)

第 12 课:调用链 — 追踪了完整的命令执行链路
第 13 课:TDD 流程 — 完成了 RED-GREEN-IMPROVE 循环
第 14 课:验证循环 — 掌握了提交前的完整验证流程
第 15 课:上下文管理 — 学会了 Token 优化和动态上下文
第 16 课:多代理编排 — 设计了并行 Agent 工作流

阶段成果:你能在真实项目中跑通完整开发流程。

第四阶段:语言与框架(第 17-22 课)

第 17 课:后端语言 — 配置了主力后端语言的 ECC
第 18 课:前端框架 — 配置了前端框架的 ECC
第 19 课:移动开发 — 了解了 Swift/Dart 的 ECC 模式
第 20 课:数据库 — 完成了 Migration 的 ECC 辅助
第 21 课:API 设计 — 掌握了符合 ECC 规范的 API 设计
第 22 课:微服务 — 理解了微服务架构的 ECC 支持

阶段成果:你能针对具体技术栈深度使用 ECC。

第五阶段:进阶能力(第 23-27 课)

第 23 课:安全威胁 — 列出了 AI 代理特有的攻击向量
第 24 课:安全防御 — 完成了 AgentShield 扫描和安全 Hook
第 25 课:持续学习 — 从会话中提取了 Instinct
第 26 课:Eval 驱动 — 设计并运行了 Eval
第 27 课:Agent 工程 — 理解了 Harness 构建和成本优化

阶段成果:你能处理安全、性能、学习、评估等高级主题。

第六阶段:综合与创造(第 28-30 课)

第 28 课:跨平台 — 理解了 Plugin Manifest 和安装 Profile
第 29 课:ECC 2.0 — 体验了 Rust 控制面板
第 30 课:毕业项目 — 为真实项目设计了完整 ECC 方案 ← 你在这里

阶段成果:你能独立设计和贡献 ECC 配置方案。


十三、写在最后

13.1 你现在拥有的能力

完成 30 课后,你具备了以下能力:

能力 说明
理解 AI Harness 知道如何增强 AI 编程助手的能力
设计配置方案 能为任何项目设计 ECC 配置
创建自定义组件 能编写 Agent、Skill、Hook
安全意识 能识别和防御 AI 代理特有的安全威胁
评估 Agent 能设计 Eval 并用 pass@k 衡量 Agent 质量
成本控制 能设计成本感知的多 Agent 工作流
跨平台迁移 能将知识应用到不同的 AI 编程助手

13.2 继续学习的方向

方向 资源
深入安全 the-security-guide.md 完整阅读
深入 Agent 开发 agent-harness-construction + autonomous-agent-harness Skill
深入持续学习 持续使用 /learn/evolve,积累 Instinct
贡献 ECC 阅读 CONTRIBUTING.md,提交你的自定义组件
关注 ECC 2.0 构建并试用 ecc2/,关注后续更新

13.3 最后的提醒

ECC 的五大原则不仅适用于 AI 编程,也适用于所有软件工程:

先规划再执行,让专家做专家的事,用测试验证结果,把安全放在首位,保持状态可控。

这是你在本课程中学到的最重要的一句话。


十四、本课小结

你应该记住的 内容
毕业项目 8 阶段 A 基础 → B 选型 → C Hook → D 验证 → E 上下文 → F 编排 → G 安全 → H 自定义
交付物 CLAUDE.md + hooks 配置 + 1 Agent + 1 Skill + 1 Hook + 说明文档
参考模板 examples/ 目录下 5 种技术栈模板
课程回顾 6 个阶段、30 课、从认知到创造的完整路径
核心原则 Plan → Agent-First → Test-Driven → Security-First → Immutability

第 29 课:ECC 2.0 — Rust 控制面板与未来方向

所属阶段:第六阶段「综合与创造」(第 28-30 课) 前置条件:第 28 课(跨平台适配与插件机制) 本课收获:理解 ECC 2.0 架构,体验 Dashboard(如环境允许)


一、本课概述

ECC 1.x 是一个基于 Node.js 脚本、Markdown 文件和 JSON 配置的插件系统。它工作得很好,但随着规模增长,有些问题开始浮现:

  • 管理多个并行会话很困难
  • 没有可视化的全局视图
  • 会话状态不持久(关掉终端就丢了)
  • Node.js 脚本在大量 Hook 并发时性能有瓶颈

ECC 2.0 用 Rust 构建了一个控制面板(Control Plane),解决这些问题:

  1. ECC 2.0 架构 — 模块设计和代码结构
  2. 核心命令 — Dashboard、会话管理、守护进程
  3. 解决的问题 — 多会话、可视化、持久化、性能
  4. 当前状态 — Alpha 质量,可构建可测试
  5. 为什么选 Rust — 性能、安全、并发

二、ECC 2.0 架构

2.1 源码结构

ecc2/src/
├── main.rs              # CLI 入口,命令定义
├── config/
│   └── mod.rs           # 配置管理(Dashboard 布局等)
├── tui/
│   ├── mod.rs           # TUI 模块入口
│   ├── app.rs           # 应用主循环
│   ├── dashboard.rs     # Terminal UI 仪表盘
│   └── widgets.rs       # 自定义 UI 组件(Token 计量、预算状态)
├── session/
│   ├── mod.rs           # 会话模块入口
│   ├── manager.rs       # 会话生命周期管理
│   ├── runtime.rs       # 会话运行时
│   ├── output.rs        # 会话输出流处理
│   ├── store.rs         # SQLite 持久化存储
│   └── daemon.rs        # 后台守护进程
├── comms/
│   └── mod.rs           # 会话间通信
├── observability/
│   └── mod.rs           # 可观测性(日志、指标)
└── worktree/
    └── mod.rs           # Git Worktree 管理

2.2 技术栈

组件 技术 用途
CLI 框架 clap 命令行参数解析
TUI 框架 ratatui 终端用户界面
异步运行时 tokio 异步 IO 和并发
数据库 rusqlite (SQLite) 会话状态持久化
时间处理 chrono 时间戳和日期
错误处理 anyhow 统一错误类型
日志 tracing + tracing-subscriber 结构化日志

2.3 模块职责

┌──────────────────────────────────────────────────┐
│                  ECC 2.0 架构                     │
│                                                   │
│  main.rs (CLI)                                    │
│  ├── dashboard  → tui/dashboard.rs (TUI 渲染)    │
│  ├── start      → session/manager.rs (创建会话)  │
│  ├── delegate   → session/manager.rs (委派会话)  │
│  ├── assign     → session/manager.rs (分配任务)  │
│  ├── sessions   → session/store.rs (查询状态)    │
│  ├── status     → session/store.rs (会话详情)    │
│  ├── stop       → session/manager.rs (停止会话)  │
│  ├── resume     → session/runtime.rs (恢复会话)  │
│  └── daemon     → session/daemon.rs (后台服务)   │
│                                                   │
│  session/store.rs ←→ SQLite 数据库               │
│  comms/mod.rs ←→ 会话间消息传递                   │
│  worktree/mod.rs ←→ Git Worktree 隔离            │
│                                                   │
└──────────────────────────────────────────────────┘

三、核心命令

3.1 命令总览

命令 功能 说明
ecc2 dashboard 启动 TUI 仪表盘 可视化所有会话状态
ecc2 start 创建新会话 指定任务、Agent 类型、是否使用 Worktree
ecc2 delegate 委派子会话 从已有会话中派生子任务
ecc2 assign 分配任务 复用已有会话或创建新会话
ecc2 sessions 列出所有会话 查看活跃/完成/失败的会话
ecc2 status 查看会话详情 包括成本、Token、运行时间
ecc2 stop 停止会话 优雅地终止正在运行的会话
ecc2 resume 恢复会话 从持久化状态恢复已暂停的会话
ecc2 daemon 启动守护进程 后台运行,管理任务调度和恢复

3.2 启动会话

# 启动一个新的 Claude 会话
ecc2 start --task "Implement user authentication module" --agent claude

# 启动并使用独立的 Git Worktree
ecc2 start --task "Refactor database layer" --agent claude --worktree

# 从已有会话委派子任务
ecc2 delegate main-session --task "Write unit tests" --agent claude --worktree

3.3 Delegate vs Assign

操作 delegate assign
行为 总是创建新会话 优先复用已有会话
适用场景 明确需要新的工作流 有可能续用之前的会话
Worktree 默认创建 按需创建

3.4 Dashboard 界面

Dashboard 是一个终端界面(TUI),使用 ratatui 构建:

┌─ ECC 2.0 Dashboard ──────────────────────────────┐
│                                                    │
│  Sessions (3 active, 2 completed)                  │
│  ┌──────────┬─────────┬────────┬────────┬────────┐│
│  │ Session  │ Agent   │ Status │ Cost   │ Tokens ││
│  ├──────────┼─────────┼────────┼────────┼────────┤│
│  │ main-01  │ claude  │ ●      │ $1.23  │ 45.2K  ││
│  │ test-02  │ claude  │ ●      │ $0.45  │ 12.8K  ││
│  │ review-3 │ claude  │ ●      │ $0.30  │ 8.1K   ││
│  │ plan-04  │ claude  │ ✓      │ $0.12  │ 3.2K   ││
│  │ fix-05   │ claude  │ ✓      │ $0.67  │ 18.9K  ││
│  └──────────┴─────────┴────────┴────────┴────────┘│
│                                                    │
│  Selected: main-01                                 │
│  Task: Implement user authentication module        │
│  Runtime: 12m 34s                                  │
│  Budget: $2.00 remaining of $5.00                  │
│                                                    │
│  Output ────────────────────────────────────────── │
│  > Creating auth middleware...                      │
│  > Running tests: 12/15 passed                     │
│  > Fixing failing test: token_expiry_test          │
│                                                    │
│  [q] Quit  [↑↓] Select  [Enter] Details  [s] Stop │
└────────────────────────────────────────────────────┘

Dashboard 的关键特性:

特性 说明
实时状态 会话状态实时更新
成本追踪 每个会话的 Token 使用和费用
输出流 选中会话的实时输出
父子关系 显示会话的委派/被委派关系
未读消息 会话间的消息通知
风险评分 Daemon 活动和调度信息

四、解决的问题

4.1 多会话管理

ECC 1.x 的痛点

终端 1:Claude Code 在做主功能开发
终端 2:另一个 Claude Code 在写测试
终端 3:还有一个在做代码审查

问题:
- 三个终端之间没有关联
- 不知道总共花了多少钱
- 一个会话的结果无法自动流转到另一个

ECC 2.0 的解决

ecc2 dashboard  ← 一个界面看到所有会话

main-session (主开发)
    ├── delegate → test-session (测试)
    └── delegate → review-session (审查)

所有会话共享状态存储,可以互相通信

4.2 可视化仪表盘

ECC 1.x 没有全局视图。你需要在多个终端之间切换才能了解整体进度。

ECC 2.0 的 Dashboard 提供了一个单一窗口查看所有会话的状态、成本、输出。

4.3 持久化

ECC 1.x:关掉终端 = 丢失会话状态。

ECC 2.0:所有会话状态存储在 SQLite 中(session/store.rs)。

pub struct StateStore {
    conn: Connection,  // SQLite 连接
}

这意味着:

  • 关掉终端后可以恢复会话(ecc2 resume
  • 可以查看历史会话的统计信息
  • Daemon 可以在后台持续管理会话

4.4 性能提升

维度 Node.js (ECC 1.x) Rust (ECC 2.0)
启动时间 ~200ms ~5ms
内存占用 ~50MB ~3MB
并发会话 受 Event Loop 限制 原生多线程
Hook 执行 进程启动开销 原生函数调用

五、Daemon 守护进程

ecc2 daemon 启动一个后台守护进程,负责:

职责 说明
任务调度 自动将待处理任务分配给空闲会话
会话恢复 检测异常退出的会话,尝试恢复
负载均衡 在多个 Lead 会话间重新平衡任务
活动记录 记录调度、恢复、重平衡的活动日志
pub struct DaemonActivity {
    pub last_dispatch_at: Option<DateTime<Utc>>,
    pub last_dispatch_routed: usize,
    pub last_dispatch_deferred: usize,
    pub last_recovery_dispatch_at: Option<DateTime<Utc>>,
    pub last_rebalance_at: Option<DateTime<Utc>>,
    pub last_rebalance_rerouted: usize,
}

六、为什么选 Rust

6.1 三个核心理由

理由 说明
性能 零成本抽象,无 GC 暂停。控制面板需要低延迟响应
安全 所有权系统防止内存错误。控制面板管理敏感会话数据
并发 Tokio 异步运行时 + Send/Sync 保证。多会话并发是核心需求

6.2 与 Node.js 的互补关系

ECC 2.0 不是要替代 Node.js,而是互补

Node.js (ECC 1.x):
  ├── Skill/Agent/Command 定义(Markdown,不需要编译)
  ├── Hook 脚本(快速迭代,不需要编译)
  └── 安装脚本(跨平台兼容性好)

Rust (ECC 2.0):
  ├── 控制面板(性能关键路径)
  ├── 会话管理(并发和持久化)
  ├── Daemon(长时间运行的后台服务)
  └── TUI Dashboard(低延迟 UI)

七、当前状态与构建

7.1 当前状态

ECC 2.0 目前处于 Alpha 质量

方面 状态
核心功能 可用(dashboard、会话管理、store)
稳定性 Alpha(可能有 Bug)
文档 基础
测试 有单元测试
安装 需要从源码构建

7.2 构建方式

# 前置条件:安装 Rust 工具链
# https://rustup.rs/

# 进入 ecc2 目录
cd ecc2/

# 构建
cargo build

# 运行测试
cargo test

# 运行 Dashboard
cargo run -- dashboard

# 或者构建 Release 版本
cargo build --release
./target/release/ecc dashboard

7.3 开发环境要求

要求 最低版本
Rust 1.75+
Cargo 随 Rust 安装
SQLite 系统自带或由 rusqlite 编译
终端 支持 256 色的终端模拟器

八、本课练习

练习 1:阅读 main.rs(10 分钟)

cat ecc2/src/main.rs

回答问题:

  • ECC 2.0 定义了哪些子命令?
  • start 命令有哪些参数?
  • delegateassign 有什么区别?

练习 2:浏览源码结构(15 分钟)

浏览 ecc2/src/ 目录,画出模块依赖关系图:

  • 哪些模块依赖 session
  • tui 模块依赖哪些其他模块?
  • store.rs 被哪些模块使用?

练习 3:构建 ECC 2.0(20 分钟,需要 Rust 环境)

如果你的环境中已安装 Rust,尝试构建并运行:

cd ecc2/
cargo build
cargo test
cargo run -- dashboard

记录:

  • 构建是否成功?
  • 测试是否全部通过?
  • Dashboard 界面是什么样的?

如果没有 Rust 环境,阅读 ecc2/src/tui/dashboard.rs 的前 50 行,理解 Dashboard 的数据结构设计。

练习 4(选做):设计改进

基于你对 ECC 2.0 架构的理解,提出一个改进建议:

  • 你认为还缺少什么功能?
  • 有什么可以优化的地方?
  • 如何提升 Dashboard 的可用性?

九、本课小结

你应该记住的 内容
核心模块 main.rs(CLI)、tui/(Dashboard)、session/(会话管理)、store.rs(SQLite)
解决的问题 多会话管理、可视化仪表盘、状态持久化、性能提升
技术栈 Rust + clap + ratatui + tokio + rusqlite
当前状态 Alpha 质量,可构建可测试
与 1.x 关系 互补而非替代:Rust 做控制面板,Node.js 做内容定义

十、下节预告

第 30 课:综合实战 — 毕业项目

这是课程的最后一课。你将把前 29 课学到的所有知识综合运用,为一个真实项目设计完整的 ECC 配置方案:选择 Profile、编写 CLAUDE.md、配置 Agent/Skill/Hook、运行完整的开发流程验证。

预习建议:选择一个你正在开发的真实项目,思考它需要什么样的 ECC 配置。浏览 examples/ 目录中的参考模板。

第 28 课:跨平台适配与插件机制

所属阶段:第六阶段「综合与创造」(第 28-30 课) 前置条件:第 11 课(Scripts 底层)、第 14-16 课(验证循环、上下文管理、多代理编排) 本课收获:理解 Plugin Manifest 格式,能选择安装 Profile


一、本课概述

从第 1 课起,我们就知道 ECC 支持 6 种 AI 编程助手。但之前的课程都聚焦于 Claude Code。本课深入跨平台的工程实现:

  1. Harness 支持表 — 每个平台支持什么、不支持什么
  2. Plugin Manifest 格式.claude-plugin/.codex-plugin/ 的结构
  3. 安装 Profile — 从 core 到 full 的五级选择
  4. JSON Schema 验证 — 如何用 Schema 确保配置正确
  5. 安装方式对比 — 不同安装方法的适用场景

理解跨平台机制后,你就具备了为任何 AI 编程助手配置 ECC 的能力。


二、Harness 支持表

2.1 六大平台支持对比

平台 支持程度 插件目录 特殊说明
Claude Code 完整支持 .claude-plugin/ 主要目标平台,所有功能均可用
Codex 完整支持 .codex-plugin/ macOS 应用和 CLI,Skill 共享
Cursor 适配支持 Hook adapter 通过适配层桥接
OpenCode 插件支持 Plugin system 原生插件系统对接
Gemini 有限支持 GEMINI.md 仅通过配置文件提供指导
Antigravity IDE 集成 IDE 内集成

2.2 功能覆盖矩阵

功能 Claude Code Codex Cursor OpenCode Gemini
Agents 全部 全部 部分 部分
Skills 全部 全部 部分 全部
Commands 全部 部分 部分
Hooks 全部 部分 适配 部分
Rules 全部 共享 共享 共享 有限
MCP 全部 全部 部分 部分

2.3 设计意义

ECC 的跨平台设计有一个核心洞察:知识是可迁移的,工具绑定是不可避免的。

  • Skill(知识)在所有平台都有价值 → 最大限度地共享
  • Hook(事件机制)依赖平台特性 → 需要适配层
  • Command(交互入口)是平台特定的 → 各自实现

三、Plugin Manifest 格式

3.1 Claude Code Plugin(.claude-plugin/plugin.json

{
  "name": "ecc",
  "version": "1.10.0",
  "description": "Battle-tested Claude Code plugin...",
  "author": {
    "name": "Affaan Mustafa",
    "url": "https://x.com/affaanmustafa"
  },
  "homepage": "https://ecc.tools",
  "repository": "https://github.com/affaan-m/everything-claude-code",
  "license": "MIT",
  "keywords": ["claude-code", "agents", "skills", "hooks", ...],
  "agents": [
    "./agents/architect.md",
    "./agents/build-error-resolver.md",
    "./agents/code-reviewer.md"
  ],
  "skills": [
    "./skills/tdd-workflow/SKILL.md",
    "./skills/security-review/SKILL.md"
  ],
  "commands": [...],
  "hooks": "./hooks/",
  "rules": "./rules/"
}

3.2 Codex Plugin(.codex-plugin/plugin.json

{
  "name": "ecc",
  "version": "1.10.0",
  "description": "Battle-tested Codex workflows...",
  "author": {
    "name": "Affaan Mustafa",
    "url": "https://x.com/affaanmustafa"
  },
  "license": "MIT",
  "skills": "./skills/",
  "mcpServers": "./.mcp.json",
  "interface": {
    "displayName": "Everything Claude Code",
    "shortDescription": "156 battle-tested ECC skills...",
    "category": "Productivity",
    "capabilities": ["Read", "Write"],
    "defaultPrompt": [
      "Use the tdd-workflow skill...",
      "Use the security-review skill...",
      "Use the verification-loop skill..."
    ]
  }
}

3.3 两种 Manifest 的差异

字段 .claude-plugin .codex-plugin 说明
agents 逐个列出 Codex 不直接支持 Agent 列表
skills 逐个列出 目录引用 Codex 用目录统一加载
commands Codex 没有 Command 概念
hooks Codex Hook 机制不同
mcpServers 隐含 显式引用 Codex 需要明确声明
interface Codex 需要 UI 展示信息
defaultPrompt Codex 支持默认提示语

设计原则:每个 Manifest 针对目标平台优化,而不是强求统一格式。


四、安装 Profile

4.1 五级 Profile

ECC 不是"要么全装、要么不装"。它提供了五个安装级别:

core → developer → security → research → full
 │         │          │          │          │
 │         │          │          │          └─ 所有组件
 │         │          │          └─ + AI/Agent 实验 Skill
 │         │          └─ + 安全扫描、审计
 │         └─ + TDD、代码审查、构建修复
 └─ 基础 Rules + 核心 Skill

4.2 Profile 详情

Profile 包含内容 适用场景 组件数量(约)
core Rules + 核心 Skill 轻量使用、低配机器 ~30
developer core + TDD/Review Agent + 开发 Skill 日常开发 ~100
security developer + 安全 Agent/Skill 安全敏感项目 ~120
research security + AI 实验 Skill AI 研究和实验 ~200
full 所有组件 完整体验、高配机器 ~400+

4.3 Profile 选择建议

问:你的机器配置如何?
  ├── 低配(<8GB RAM) → core 或 developer
  └── 正常/高配 → 继续

问:你是否需要安全扫描?
  ├── 是 → security 或更高
  └── 否 → developer

问:你是否在做 AI/Agent 开发?
  ├── 是 → research 或 full
  └── 否 → security 或 developer

问:你是否想体验所有功能?
  ├── 是 → full(注意性能影响)
  └── 否 → 按需选择

重要警告full Profile 在低配机器上可能导致性能问题,因为加载大量 Skill 和 Agent 会占用上下文窗口和内存。


五、JSON Schema 验证

ECC 使用 JSON Schema 来验证各种配置文件的正确性。

5.1 Schema 文件清单

schemas/ 目录中的 Schema 文件:

Schema 文件 验证目标 作用
plugin.schema.json Plugin Manifest 验证插件配置格式
hooks.schema.json Hook 配置 验证 Hook 定义格式
install-profiles.schema.json 安装 Profile 验证 Profile 配置
install-components.schema.json 安装组件 验证组件清单
install-modules.schema.json 安装模块 验证模块定义
install-state.schema.json 安装状态 验证安装记录
ecc-install-config.schema.json 安装配置 验证整体安装配置
package-manager.schema.json 包管理器 验证包管理器配置
provenance.schema.json 来源追踪 验证组件来源信息
state-store.schema.json 状态存储 验证状态持久化

5.2 Schema 验证的价值

没有 Schema:
  修改了 hooks.json 中的一个字段名
      → 运行时才发现 Hook 不生效
      → 调试半小时才找到原因

有 Schema:
  修改了 hooks.json 中的一个字段名
      → 编辑器即时提示"字段名不合法"
      → 1 秒修复

5.3 使用方式

在支持 JSON Schema 的编辑器(如 VS Code)中,将 Schema 关联到对应的 JSON 文件:

// .vscode/settings.json
{
  "json.schemas": [
    {
      "fileMatch": ["hooks.json", ".claude/hooks.json"],
      "url": "./schemas/hooks.schema.json"
    },
    {
      "fileMatch": [".claude-plugin/plugin.json"],
      "url": "./schemas/plugin.schema.json"
    }
  ]
}

六、安装方式对比

6.1 四种安装方式

方式 命令 适用场景 可定制性
一键安装 npx ecc-install 快速体验
Profile 安装 npx ecc-install --profile developer 按需安装
选择性安装 npx ecc-install --select 精细控制
手动安装 复制文件 完全控制 最高

6.2 安装位置

组件类型 全局安装位置 项目安装位置
Rules ~/.claude/rules/ .claude/rules/
Agents ~/.claude/agents/ .claude/agents/
Skills ~/.claude/skills/ .claude/skills/
Hooks ~/.claude/settings.json .claude/settings.json
Commands ~/.claude/commands/ .claude/commands/

优先级:项目级配置 > 全局配置。当两者冲突时,项目级生效。

6.3 手动安装注意事项

手动安装时的关键警告:

# ✅ 正确:复制整个目录
cp -r rules/common ~/.claude/rules/common
cp -r rules/typescript ~/.claude/rules/typescript

# ❌ 错误:用 /* 展平目录
cp -r rules/common/* ~/.claude/rules/
cp -r rules/typescript/* ~/.claude/rules/
# 这会导致语言特定文件覆盖通用文件!
# 因为 common/ 和 typescript/ 中有同名文件

七、本课练习

练习 1:对比 Plugin Manifest(15 分钟)

这是本课最重要的练习。

阅读 .claude-plugin/plugin.json.codex-plugin/plugin.json,列出至少 3 个关键差异:

# 查看 Claude Code Plugin
cat .claude-plugin/plugin.json

# 查看 Codex Plugin
cat .codex-plugin/plugin.json

对每个差异,解释为什么两个平台需要不同的设计。

练习 2:选择 Profile(10 分钟)

根据你自己的开发场景,选择一个安装 Profile,并解释为什么:

  • 你的机器配置如何?
  • 你最常用的工作流是什么?
  • 你是否需要安全扫描功能?
  • 你是否在做 AI Agent 开发?

练习 3:浏览 Schema(10 分钟)

打开 schemas/hooks.schema.json,回答:

  • Hook 配置中有哪些必填字段?
  • matcher 字段的合法值有哪些?
  • async 字段的默认值是什么?

练习 4(选做):设计跨平台策略

假设你的团队同时使用 Claude Code 和 Codex。设计一个策略:

  • 哪些组件应该共享?
  • 哪些组件需要各自维护?
  • 如何保持两个平台的配置同步?

八、本课小结

你应该记住的 内容
支持平台 6 种:Claude Code(完整)、Codex(完整)、Cursor(适配)、OpenCode、Gemini、Antigravity
Manifest 差异 Claude Code 列举组件,Codex 目录引用 + interface 字段
五级 Profile core → developer → security → research → full
Schema 验证 schemas/ 目录下 10 个 Schema 文件保证配置正确性
安装原则 复制整个目录,不要用 /* 展平

九、下节预告

第 29 课:ECC 2.0 — Rust 控制面板与未来方向

跨平台适配是 ECC 1.x 的方案。ECC 2.0 用 Rust 重写了控制面板,引入了 TUI 仪表盘、多会话管理、SQLite 持久化。下节课你将了解 ECC 2.0 的架构、核心命令,以及为什么选择 Rust。

预习建议:浏览 ecc2/src/main.rs,感受一下 ECC 2.0 的命令结构。

第 27 课:Agent 工程与 LLM 成本优化

所属阶段:第五阶段「进阶能力」(第 23-27 课) 前置条件:第 7 课(Agent 设计)、第 15 课(模型选择)、第 26 课(Eval) 本课收获:理解 Agent Harness 构建原理和 LLM 成本优化策略


一、本课概述

前几课你学了安全、学习、评估。本课进入 Agent 工程的核心 — 构建高质量 Agent 的工程方法论控制 LLM 使用成本

  1. Agent Harness 构建 — Action Space、Observation 格式化、Recovery 设计
  2. 成本感知 LLM Pipeline — 按复杂度路由模型、预算追踪、Retry 逻辑
  3. AI Skill 全景 — ECC 中所有 AI 相关 Skill 的全图
  4. 自主循环质量门 — 确保自主运行的 Agent 不失控

掌握这些知识后,你就具备了设计和优化工业级 Agent 系统的能力。


二、Agent Harness 构建

ECC 的 agent-harness-construction Skill 定义了 Agent 质量的四个约束维度:

Agent 输出质量 = f(Action Space, Observation, Recovery, Context Budget)

2.1 Action Space 设计

Action Space(动作空间)是 Agent 可以使用的工具集合。设计原则:

原则 说明 示例
名称稳定且显式 工具名一看就知道干什么 search_code 而非 tool_3
输入 Schema 优先 用 JSON Schema 定义参数 类型检查 + 必填项
输出形状确定 返回结构固定 { status, data, error }
避免万能工具 除非隔离不可能 拆分 do_everything 为多个专用工具

2.2 工具粒度规则

不是所有工具都应该是同一粒度:

┌─────────────────────────────────────────────┐
│           工具粒度光谱                        │
│                                              │
│  微工具(Micro)                             │
│  ├── 用途:高风险操作                        │
│  ├── 示例:deploy、migrate、set_permission   │
│  └── 特点:权限小、操作原子、回滚容易        │
│                                              │
│  中工具(Medium)                            │
│  ├── 用途:常见编辑/读取/搜索                │
│  ├── 示例:edit_file、search_code、run_test  │
│  └── 特点:频率高、效率和安全平衡            │
│                                              │
│  宏工具(Macro)                             │
│  ├── 用途:来回通信成本是主要瓶颈时          │
│  ├── 示例:batch_edit、full_test_suite       │
│  └── 特点:一次做很多,减少轮次              │
│                                              │
└─────────────────────────────────────────────┘

2.3 Observation 设计

每个工具的返回值(Observation)应该包含四个字段:

{
  "status": "success",
  "summary": "Found 3 files matching pattern *.test.js",
  "next_actions": [
    "Run tests with: node tests/run-all.js",
    "Check coverage with: npx c8 node tests/run-all.js"
  ],
  "artifacts": [
    "tests/lib/utils.test.js",
    "tests/lib/package-manager.test.js",
    "tests/hooks/hooks.test.js"
  ]
}
字段 作用 缺失后果
status 告诉 Agent 是否成功 Agent 不知道要不要重试
summary 一行总结结果 Agent 需要解析大量原始输出
next_actions 建议下一步操作 Agent 可能走错方向
artifacts 文件路径/ID Agent 无法引用具体产物

2.4 Error Recovery 设计

每个错误路径必须包含三个元素:

错误发生
    ├── root_cause_hint(根因提示)
    │   "File not found: tests/foo.test.js — check if path is relative"
    │
    ├── safe_retry_instruction(安全重试指令)
    │   "Retry with absolute path: /Users/dev/project/tests/foo.test.js"
    │
    └── explicit_stop_condition(明确停止条件)
        "If file still not found after 2 retries, report error and stop"

没有停止条件的 Agent 会无限重试 — 这是 Agent 工程中最常见的失败模式之一。

2.5 架构模式选择

模式 适用场景 特点
ReAct 探索性任务,路径不确定 灵活,但可能发散
Function-calling 结构化确定性流程 高效,但不灵活
Hybrid(推荐) 大多数场景 ReAct 规划 + 类型化工具执行

三、成本感知 LLM Pipeline

cost-aware-llm-pipeline Skill 提供了控制 LLM API 成本的完整方案。

3.1 按复杂度路由模型

核心思路:不是所有任务都需要最贵的模型。

# 模型选择逻辑(简化版)
def select_model(text_length, item_count, force_model=None):
    if force_model is not None:
        return force_model
    if text_length >= 10_000 or item_count >= 30:
        return "claude-sonnet-4-6"   # 复杂任务用 Sonnet
    return "claude-haiku-4-5"         # 简单任务用 Haiku(3-4x 便宜)

ECC 中的模型选择策略(回顾第 15 课):

模型 定位 典型用途 相对成本
Haiku 4.5 轻量快速 后台 Agent、代码生成、Instinct 分析 1x
Sonnet 4.6 主力编程 主开发任务、编排工作流 3-4x
Opus 4.5 深度推理 架构决策、复杂研究 15-20x

3.2 不可变成本追踪

遵循 ECC 的不可变原则,成本追踪使用冻结数据类

from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class CostRecord:
    model: str
    input_tokens: int
    output_tokens: int
    cost_usd: float

@dataclass(frozen=True, slots=True)
class CostTracker:
    budget_limit: float = 1.00
    records: tuple[CostRecord, ...] = ()

    @property
    def total_cost(self) -> float:
        return sum(r.cost_usd for r in self.records)

    @property
    def budget_remaining(self) -> float:
        return self.budget_limit - self.total_cost

    def add(self, record: CostRecord) -> 'CostTracker':
        # 返回新对象,不修改原对象
        return CostTracker(
            budget_limit=self.budget_limit,
            records=self.records + (record,),
        )

3.3 预算追踪与超限保护

每次 API 调用:
    1. 选择模型(按复杂度)
    2. 调用 API
    3. 记录成本(创建新 CostRecord)
    4. 更新 Tracker(返回新 CostTracker)
    5. 检查预算
        ├── 剩余 > 20% → 继续
        ├── 剩余 10-20% → 切换到更便宜的模型
        └── 剩余 < 10% → 停止,报告预算耗尽

3.4 Retry 逻辑

API 调用可能因为网络、速率限制等原因失败。成本感知的 Retry 策略:

错误类型 重试策略 成本考量
429 Rate Limit 指数退避重试 不额外计费
500 Server Error 最多重试 2 次 可能计费
超时 重试 1 次 可能计费
内容被过滤 调整 Prompt 后重试 计费
预算不足 降级到更便宜的模型 减少计费

3.5 Prompt Caching

当多次调用使用相同的 System Prompt 时,利用 Prompt Caching 节省成本:

1 次调用:发送完整 System Prompt → 缓存命中 0%2 次调用:System Prompt 已缓存 → 只计费增量部分
    ↓
对于有大量 Skill 的 System Prompt,缓存可以节省 60-80% 的输入 Token 费用

四、AI Skill 全景表

ECC 中与 AI/Agent 开发相关的 Skill 全景:

Skill 聚焦领域 关键内容
claude-api Claude API 使用 API 调用模式、消息格式、Token 计算
agentic-engineering Agent 工程方法论 Agent 设计原则、编排模式
ai-first-engineering AI 优先的开发方式 如何让 AI 成为开发流程核心
autonomous-loops 自主循环设计 无人监督的 Agent 循环架构
agent-introspection-debugging Agent 调试 如何诊断 Agent 行为异常
agent-eval Agent 评估 Agent-vs-Agent 评估方法
agent-payment-x402 Agent 支付 AI Agent 的支付协议(x402)
autonomous-agent-harness 自主 Agent 框架 完整的自主 Agent 构建方案
continuous-agent-loop 持续 Agent 循环 长时间运行的 Agent 设计
agent-harness-construction Harness 构建 Action Space / Observation / Recovery
cost-aware-llm-pipeline 成本优化 模型路由、预算追踪、Prompt Caching

五、自主循环质量门

当 Agent 在无人监督下自主运行时,必须有质量门(Quality Gate)防止失控:

5.1 每轮退出条件

while (任务未完成) {
    执行一轮操作

    // 质量门检查
    if (轮次 >= 最大轮次) → 停止,报告 "达到最大轮次"
    if (成本 >= 预算上限) → 停止,报告 "预算耗尽"
    if (连续失败 >= 3) → 停止,报告 "连续失败"
    if (输出验证失败) → 回退到上一个检查点
    if (安全检查失败) → 立即停止
}

5.2 质量门清单

检查内容 触发动作
轮次门 当前轮次 < 最大轮次 超限 → 强制停止
成本门 累计成本 < 预算上限 超限 → 停止或降级
失败门 连续失败次数 < 阈值 超限 → 停止并报告
输出门 输出通过验证检查 失败 → 回退重试
安全门 无危险操作 失败 → 立即停止
漂移门 输出与预期方向一致 偏移 → 告警或停止

5.3 为什么质量门是必须的

没有质量门的自主 Agent:

❌ 一个 Agent 被要求"优化代码性能"
   → 它开始重构整个代码库
   → 消耗了 $50 的 API 费用
   → 引入了 12 个新 Bug
   → 没有人发现直到第二天

有质量门的自主 Agent:

✅ 同一个 Agent 被要求"优化代码性能"
   → 第 3 轮:成本达到预算 50%,切换到 Haiku
   → 第 5 轮:测试失败,回退到上一个检查点
   → 第 8 轮:达到最大轮次,停止并输出报告
   → 总成本:$2.30,所有测试通过

六、多 Agent 成本预算设计

6.1 预算分配策略

对于多 Agent 工作流,预算需要提前分配:

总预算:$10.00
    │
    ├── planner Agent:     $0.50 (5%)   — Haiku,快速规划
    ├── 主开发 Agent:      $5.00 (50%)  — Sonnet,核心工作
    ├── code-reviewer:     $1.50 (15%)  — Sonnet,审查质量
    ├── security-reviewer: $1.00 (10%)  — Sonnet,安全审查
    ├── tdd-guide:         $1.50 (15%)  — Sonnet,测试驱动
    └── 预留缓冲:         $0.50 (5%)   — 应对重试和意外

6.2 预算追踪表格

Agent 模型 预算 已用 剩余 状态
planner Haiku $0.50 $0.12 $0.38 正常
developer Sonnet $5.00 $3.80 $1.20 注意
reviewer Sonnet $1.50 $0.90 $0.60 正常
security Sonnet $1.00 $0.00 $1.00 未启动
tdd Sonnet $1.50 $1.20 $0.30 警告
合计 $10.00 $6.02 $3.98

七、本课练习

练习 1:设计 Action Space(15 分钟)

为一个"自动化代码迁移 Agent"设计 Action Space:

  • 列出它需要的 5-8 个工具
  • 为每个工具定义输入/输出 Schema
  • 标注粒度级别(微/中/宏)

练习 2:成本预算设计(20 分钟)

这是本课最重要的练习。

为一个包含 3 个 Agent 的工作流设计成本预算:

  • planner(规划任务拆分)
  • developer(编写代码)
  • reviewer(审查代码)

假设总预算 $5.00,每个 Agent 应该分配多少?使用什么模型?预留多少缓冲?

练习 3:质量门设计(15 分钟)

为一个"自主运行的安全扫描 Agent"设计质量门:

  • 最大运行轮次是多少?
  • 成本上限是多少?
  • 什么情况下应该立即停止?
  • 什么情况下可以降级继续?

练习 4(选做):Observation 格式对比

阅读 agent-harness-construction Skill,然后对比以下两种 Observation 格式,说明哪种更好以及为什么:

格式 A:

Found 3 files. The files are located in the tests directory.

格式 B:

{"status": "success", "summary": "Found 3 test files", "artifacts": ["tests/a.js", "tests/b.js", "tests/c.js"], "next_actions": ["Run tests: node tests/run-all.js"]}

八、本课小结

你应该记住的 内容
Harness 四维度 Action Space + Observation + Recovery + Context Budget
工具粒度 高风险用微工具、常见操作用中工具、通信瓶颈用宏工具
成本路由 简单任务用 Haiku(便宜 3-4x),复杂任务用 Sonnet
不可变追踪 每次 API 调用返回新的 CostTracker,不修改原对象
质量门 每轮必须检查:轮次、成本、失败次数、输出验证、安全

九、下节预告

第 28 课:跨平台适配与插件机制

Agent 工程是"构建"的能力。下一课我们学习"分发"的能力 — ECC 如何适配 6 种不同的 AI 编程助手,Plugin Manifest 的格式设计,以及安装 Profile 的选择策略。

预习建议:浏览 .claude-plugin/plugin.json.codex-plugin/plugin.json,比较两者的结构差异。

第 26 课:Eval 驱动开发 — 衡量 AI 行为

所属阶段:第五阶段「进阶能力」(第 23-27 课) 前置条件:第 25 课(持续学习) 本课收获:设计并运行一次 Eval,理解 pass@k 指标体系


一、本课概述

上一课你学到了如何让 AI "学习"。但学习的效果如何衡量?一个 Agent 配置调整后,是变好了还是变差了?

传统软件有单元测试来验证功能正确性。AI Agent 需要的不是单元测试,而是 Eval(评估)

  1. EDD 循环 — Eval 驱动开发的完整流程
  2. pass@k 指标 — 理解 AI 行为的概率性质
  3. Eval 类型 — Checkpoint-based、Continuous、Agent-vs-Agent
  4. benchmark Skill — 性能基准与回归检测
  5. verification-loop Skill — 验证闭环

Eval 是连接"AI 能力"和"工程可靠性"的桥梁。


二、为什么需要 Eval

2.1 传统测试 vs AI Eval

维度 传统单元测试 AI Eval
确定性 相同输入 → 相同输出 相同输入 → 可能不同输出
通过标准 全部通过 = 正确 通过率达标 = 可靠
重复次数 运行 1 次即可 需要多次运行取统计
评判方式 精确匹配 语义评判(可能需要另一个 LLM)
失败原因 Bug 可能是 Prompt、模型、上下文、温度...

2.2 AI 行为的概率性

一个关键认知:AI Agent 的行为是概率性的,不是确定性的。

同一个 Prompt + 同一个 Agent + 同一个模型,运行 10 次可能得到 7 次正确、2 次部分正确、1 次错误。

这意味着:

  • 你不能只运行一次就下结论
  • "通过率"比"通过/不通过"更有意义
  • 你需要 pass@k 这样的统计指标

三、EDD 循环

Eval 驱动开发(Eval-Driven Development,EDD)是一个迭代循环:

┌──────────────────────────────────────────┐
│                EDD 循环                   │
│                                          │
│  1. 定义评估标准                          │
│     │ 什么算"通过"?什么算"失败"?       │
│     ↓                                    │
│  2. 运行 Eval                            │
│     │ 多次运行,收集结果                  │
│     ↓                                    │
│  3. 分析 pass@k                          │
│     │ 计算通过率,识别薄弱环节            │
│     ↓                                    │
│  4. 调整配置                              │
│     │ 修改 Prompt / Skill / Agent / 模型 │
│     ↓                                    │
│  5. 重新运行 Eval                        │
│     │ 验证改进效果                        │
│     ↓                                    │
│  6. 达标?                               │
│     │                                    │
│     ├── 否 → 回到步骤 4                  │
│     └── 是 → 部署                        │
│                                          │
└──────────────────────────────────────────┘

3.1 定义评估标准

评估标准应该是具体的、可衡量的

## Eval: Code Reviewer Agent

### 测试用例 1:检测未处理的空指针
- 输入:包含空指针解引用的 Java 代码
- 期望:Agent 识别出空指针风险并给出修复建议
- Pass 条件:输出中包含 "null check" 或 "NullPointerException"

### 测试用例 2:检测 SQL 注入
- 输入:包含字符串拼接 SQL 的 Python 代码
- 期望:Agent 识别出 SQL 注入风险
- Pass 条件:输出中包含 "parameterized" 或 "SQL injection"

### 测试用例 3:不误报安全代码
- 输入:已经使用参数化查询的代码
- 期望:Agent 不报告 SQL 注入
- Pass 条件:输出中不包含 "SQL injection"

3.2 好的评估标准特征

特征 说明 反例
具体 明确的 pass/fail 条件 "代码质量应该好"
可自动判断 可以用程序检查 需要人工阅读评判
独立 测试用例之间不互相依赖 "测试 2 依赖测试 1 的输出"
覆盖正反面 既测能力也测不误报 只测能检测出问题

四、pass@k 指标体系

4.1 三种 pass@k 指标

指标 定义 衡量什么 严格程度
pass@1 单次运行通过率 可靠性 中等
pass@5 5 次中至少 1 次通过 极限能力 宽松
pass^k k 次全部通过 一致性 最严格

4.2 直观理解

假设一个 Agent 运行 10 次,结果如下:

运行 1:  通过
运行 2:  通过
运行 3:  失败
运行 4:  通过
运行 5:  通过
运行 6:  失败
运行 7:  通过
运行 8:  通过
运行 9:  通过
运行 10:  失败
指标 计算 结果
pass@1 7/10 70% — 单次运行有 70% 概率成功
pass@5 至少 1 次通过的概率 99.8% — 跑 5 次几乎肯定能成功
pass^5 全部 5 次通过的概率 16.8% — 连续 5 次都成功很难

4.3 选择哪个指标

场景 推荐指标 原因
代码审查(可以人工兜底) pass@1 ≥ 80% 大部分时候对就行
安全扫描(不能漏报) pass@5 ≥ 99% 运行多次,确保不遗漏
自主部署(无人监督) pass^3 ≥ 95% 必须极其一致
探索性任务(找方案) pass@5 ≥ 60% 只要有一次找到就好

五、Eval 类型

5.1 Checkpoint-based Eval

在开发流程的关键节点运行评估:

代码变更 → Eval(代码审查 Agent 能否检测已知问题?)
                    ↓
配置修改 → Eval(修改后的 Agent 表现是否不低于之前?)
                    ↓
模型升级 → Eval(新模型下 Agent 表现是否不低于之前?)

用途:验证配置变更不会导致退化。

5.2 Continuous Eval

持续运行的评估,类似于持续集成:

每天自动运行一组 Eval 用例
    ↓
收集 pass@k 趋势数据
    ↓
发现退化时自动告警

用途:监控 Agent 行为的长期趋势。

5.3 Agent-vs-Agent Eval

用一个 Agent 评估另一个 Agent 的输出:

被评估 Agent:生成代码审查报告
    ↓
评估 Agent(Evaluator):判断报告是否正确
    ↓
对比人类标注的标准答案
    ↓
计算 Evaluator 的一致性

ECC 提供了 agent-eval Skill 来支持这种模式。

注意:Agent-vs-Agent 评估本身也有不确定性。评估 Agent 可能误判。所以需要用人类标注的"黄金答案"来校准评估 Agent。


六、benchmark Skill

ECC 的 benchmark Skill 提供了性能基准和回归检测能力:

6.1 核心功能

功能 说明
基准建立 在已知良好状态下运行 Eval,记录基线
回归检测 变更后重新运行,对比基线
趋势分析 多次运行结果的趋势图
阈值告警 pass@k 低于阈值时告警

6.2 使用示例

## 建立基准

1. 确保当前 Agent 配置是"已知良好"的状态
2. 运行 benchmark Skill 建立基线
3. 记录 pass@1、pass@5 值

## 检测回归

1. 修改 Agent 配置(Prompt、模型、Skill 等)
2. 使用相同的 Eval 用例重新运行
3. 对比新结果与基线
4. 如果 pass@k 下降超过 5%,调查原因

七、verification-loop Skill

verification-loop Skill 提供了一个自动化的验证闭环:

执行任务
    ↓
自动验证结果(测试、Lint、类型检查、安全扫描)
    ↓
    ├── 全部通过 → 完成
    └── 有失败 → 自动修复 → 重新验证(最多 N 轮)

7.1 与 Eval 的关系

维度 verification-loop Eval
时机 开发过程中(实时) 开发完成后(事后)
目标 确保当前任务正确 衡量 Agent 整体能力
频率 每次任务 定期或变更后
反馈 自动修复 指导配置调整

两者是互补的:verification-loop 保证单次任务质量,Eval 保证 Agent 长期可靠性。


八、设计 Eval 的最佳实践

8.1 Eval 设计清单

# 要点 说明
1 覆盖正面和负面 既测"能检测出"也测"不误报"
2 用例数量 ≥ 10 少于 10 个统计意义不足
3 明确 pass/fail 条件 可以用字符串匹配或语义判断
4 运行次数 ≥ 5 取统计平均,不依赖单次结果
5 设定基线 知道"现在有多好"才能判断"变好还是变差"
6 固定随机种子 如果可能,减少不确定性来源
7 记录环境 模型版本、温度、Prompt 版本

8.2 Eval 结果分析模板

## Eval Report: [Agent Name] - [Date]

### 环境
- Model: claude-sonnet-4-6
- Temperature: 0
- Prompt version: v2.3
- Eval cases: 15
- Runs per case: 5

### 结果
| 指标 | 值 | 基线 | 变化 |
|------|----|------|------|
| pass@1 | 82% | 78% | +4% ✅ |
| pass@5 | 98% | 96% | +2% ✅ |
| pass^3 | 65% | 60% | +5% ✅ |

### 薄弱环节
- 用例 #7(复杂嵌套代码):pass@1 仅 40%
- 用例 #12(多文件变更):pass@1 仅 60%

### 下一步
- 针对用例 #7 增加 Skill 指导
- 针对用例 #12 考虑拆分为子任务

九、本课练习

练习 1:设计 3 个 Eval 用例(20 分钟)

这是本课最重要的练习。

为一个你使用的 Agent(如 code-reviewer)设计 3 个评估用例。每个用例包括:

字段 内容
测试名称 描述性名称
输入 提供给 Agent 的代码或问题
期望行为 Agent 应该做什么
Pass 条件 具体的通过标准
Fail 条件 什么情况算失败

练习 2:理解 pass@k(10 分钟)

一个 Agent 运行 20 次,有 14 次通过。计算:

  • pass@1 = ?
  • 如果 pass@5 的目标是 ≥ 99%,这个 Agent 达标了吗?
  • 如果 pass^3 的目标是 ≥ 50%,这个 Agent 达标了吗?

(提示:pass@5 = 1 - (失败率)^5 = 1 - 0.3^5;pass^3 = (通过率)^3 = 0.7^3)

练习 3:EDD 循环实践(15 分钟)

选择一个简单的任务(如"让 Claude 把 JSON 转换为 YAML"),执行一个简化的 EDD 循环:

  1. 定义 pass 条件(输出是合法的 YAML)
  2. 运行 3 次
  3. 记录 pass@1
  4. 如果不理想,调整 Prompt 后再运行 3 次
  5. 对比前后结果

练习 4(选做):Agent-vs-Agent

思考:如果让一个 Agent 来判断另一个 Agent 的代码审查质量,你会怎么设计评估 Prompt?评估 Agent 自身的准确性怎么衡量?


十、本课小结

你应该记住的 内容
EDD 循环 定义标准 → 运行 Eval → 分析 pass@k → 调整 → 重跑 → 达标 → 部署
pass@1 单次通过率,衡量日常可靠性
pass@5 至少 1 次通过,衡量极限能力
pass^k 全部通过,最严格的一致性要求
Eval 类型 Checkpoint(变更验证)、Continuous(长期监控)、Agent-vs-Agent

十一、下节预告

第 27 课:Agent 工程与 LLM 成本优化

Eval 告诉你 Agent 有多好,但"好"不能不计成本。下节课学习 Agent Harness 的构建原理(Action Space、Observation 格式化、Reward Signal),以及如何用 cost-aware-llm-pipeline Skill 按复杂度路由模型、追踪预算、优化成本。

预习建议:浏览 skills/agent-harness-construction/SKILL.mdskills/cost-aware-llm-pipeline/SKILL.md

❌