Bhe's Blog

Bhe's Blog

这是 Agent Views 系列的第一篇文章,专注探讨 Agent 技术的原理与实践。

从 Slash Command 说起

在聊 Skill 之前,我们先回头说说 Claude Code 里的一个老功能:Slash Command。

Slash Command 可以说就是一个 prompt 模板或快捷方式,作用是让你很方便地往代码仓库里加入指令。

比如说:

  • 帮我提交一个 git commit, log 规范是 xxxx
  • 使用命令 xx-lint 对其进行格式化 $arguments
  • 检查语法 $arguments

这些实际上都是非常明确的指令, 你需要每天输入, 太重复了, 需要快捷方式。由于它就是自动提交一段 Prompt, 也可以用来干预 agent 的运行风格, 起到定制 Agent 行为的作用.

业界已经有不少成熟的实践,例如 BMAD 项目就采用这种方式, 提供大量的 Agent 角色指令, 可以通过 slash command 影响 agent 的行为; open spec 也是通过 command 的方式管理提案内容

这类东西本身就是一段经常要输入的 Prompt, 本来需要手动输入, 现在通过 / 命令可以很方便地重复使用.

但 Slash Command 有个明确边界:必须由人主动触发。

所谓 command,其实就是”你下发命令,它才会执行”。

所以 Slash Command 作为固定工具,是用户的”工具库”:你需要时可以拿来用,但模型不会主动使用(虽然也是可以要求它直接使用这些md文件),只能人为触发。因此,Slash Command 更多是快捷方式,而不是 Agent 自发地使用。

Subagent 机制的出现

后来 Claude 推出了很多新功能。其中一个大家经常提到就是 MCP,但这里必须说:真正重要的是, 之后 Claude Code 引入的 subagent 机制。

Subagent 的核心在于:你可以定义完全属于自己的 Agent。不同于在命令行或对话窗口直接交互的 Agent(即 main agent 或 task agent)。

Subagent 的”sub”意味着:它作为子 Agent 存在,由用户或 main agent 主动创建调度,完成具体封闭的任务。

比如常见例子:explore agent。Explore agent 作用是探索文件目录,寻找目标相关文件并读取理解内容。

我们原来已有 glob 工具。Glob 实际上是通配符文件搜索工具,是内置 tool 用于搜索文件。

两者可以对比:

Glob tool 的工作方式:

  • 性质:内置工具
  • 流程
    1. 模型先确定搜索内容
    2. 生成 tool_use
    3. 调用 tool 执行搜索

这和调用 bash 工具实际上相同:模型必须先完整确定参数再调用工具。

而新 explore subagent 特点在于:

  • 性质: 也是 tool,名叫 task,是通用工具。
  • 流程:
    1. 模型生成 task tool 的任务描述
    2. task tool 启动通用agent, 并使用 explore agent 作为system prompt
    3. sub agent 完成多个文件的读取和内容分析
    4. sub agent 返回结果给 main agent

在传入 explore subagent 描述后,就变成:

task tool 中运行、拥有独立上下文的 Sub Agent。

这 SubAgent 与最外层 main agent 之间是一种无法直接交互的关系,前者仅能向后者回传最终结果。。

这是不是很像远程调用和 MCP:你调用它,不关心中间过程,它可能调用多个模型、工具,最终只返回最终结果。

不同的是:Subagent 相当于运行了一个小的 claude code, 大多数操作仍在本地。

如配置允许,subagent 也可用其他 tool,读取文件、执行操作。但对主 Agent 来说,这些过程只是展示,不会写入上下文。

所以 subagent 核心:把事情封装成独立子 Agent 并隔离上下文。不会污染主 Agent 上下文,这点很重要。

Token 并未节省——因为背后另外跑了一个 Reactive Agent 流程。但是这些 LLM message 不会引入主流程上下文空间里。

我们知道长流程的上下文有限,如 200K。如果所有事都堆在主 Agent 里,很快就会耗尽。

而有 subagent:可以把任务拆分成多步,分别由不同 subagent 执行,或对同一任务并发执行。主 Agent 只收结果,不承载过程。

从这个角度看,subagent 很像编程中的 class:封装 + 隔离。

所以, 接下来我们就讲讲 Agent 里的 Trait , 组合拳 , 也就是 skill.

Skill 的核心特性

接下来是最近全新推出的 Skill。Skill 实际上仍是 tool,和 MCP、bash、subagent 一样都是工具形态。

但 Skill 最大不同:它是触发式、按需加载的, 而且它本身不直接执行任务.

当你在对话中提与 Skill 相关内容时,模型会感知:当前指令和某 Skill 相关, 于是加载 Skill 的 Prompt, 继续执行任务。

这背后体现了一个重要的交互设计概念——渐进式披露(Progressive Disclosure),即允许模型根据当前任务的需要,动态加载相应的能力。

回顾之前方式:Slash Command、用户手写 prompt, 或者 CLAUDE.md,都是主动灌信息给模型。

而 Sub Agent/Skill 可通过模型可主动或被动加载.

实际上, 模型通过 Task 和 Skill tool 的描述信息, 感知到当前具备的动态部分.

再对比 MCP:MCP 必须作为 tool 整体加载上下文。你 MCP 里有多少方法,就必须全告诉模型。

模型需要在”知道所有MCP接口” 前提下,才能决定调哪个 MCP、用什么参数。

结果导致 MCP 非常像传统的静态链接库——必须在启动时一次性全量加载。当集成的 MCP 服务增多时,上下文窗口会迅速膨胀。

于是业界又衍生出相关产品:MCP 代理、MCP 封装层。它们做的事就是帮你筛选、转发 MCP 调用。

这些中间层的目的只有一个:缓解 MCP 无法按需加载问题。

而 Skill 恰好很好规避了这点, 因为Skill 是按需触发的,提到相关内容时模型才加载。

Skill 结构很简单:SKILL.md, 提供名字、描述使用场景, 以及具体内容.

除了 MD 文档,还可以提供 Python 脚本或 Shell 脚本。告知模型可以通过哪些工具或者脚本来完成任务。

当模型完成了加载 SKILL , 了解到新的 “可用某命令”,然后自主地通过 bash 执行该操作。

在完成 Skill 加载之后, Agent 可以进一步与 bash、其他 tool 以及 subagent 开始形成交叉组合.

Skill 的创建方式

聊完 Skill 基本情况,继续展开说 Skill 如何创建

在官方提供的 skill 市场中,创建 Skill 用 Skill Creator 工具。

https://github.com/anthropics/skills

Skill Creator 本身简单规范。

1
2
3
4
5
6
7
8
9
10
./marketplaces/anthropic-agent-skills/skills/skill-creator
./marketplaces/anthropic-agent-skills/skills/skill-creator/references
./marketplaces/anthropic-agent-skills/skills/skill-creator/references/workflows.md
./marketplaces/anthropic-agent-skills/skills/skill-creator/references/output-patterns.md
./marketplaces/anthropic-agent-skills/skills/skill-creator/scripts
./marketplaces/anthropic-agent-skills/skills/skill-creator/scripts/init_skill.py
./marketplaces/anthropic-agent-skills/skills/skill-creator/scripts/package_skill.py
./marketplaces/anthropic-agent-skills/skills/skill-creator/scripts/quick_validate.py
./marketplaces/anthropic-agent-skills/skills/skill-creator/SKILL.md
./marketplaces/anthropic-agent-skills/skills/skill-creator/LICENSE.txt

第一步,它提供 init_skill Python 脚本。通过此脚本可快速初始化 Skill 模板结构,包括:若干默认文件夹、默认 skill.md 文件。

接下来根据需求编写 skill.md 文件,明确说明:这 Skill 做什么、使用规范、解决问题。

内容写清后,如模板中有用不到的文件目录,Skill Creator 会自动删除多余内容。

所以某种意义上,Skill Creator 本身就是很好的 Skill 范本。既可用来创建 Skill,也可用于学习.

它同时也展示了一个”好 Skill”如何设计组织。


Skill 带来的变革

到这里是 Skill 入门部分。接下来看更重要问题:Skill 带来哪些变化?

第一点前面已提:Skill 是渐进式披露、可动态加载的。同时还能与现有 Agent 能力自然结合。

实现上看很简单:动态加载 Skill、读取 Markdown 文件、知目录下含哪些脚本。这些信息直接提供给运行中 Agent,从而增强能力

传统方式的局限性

Skill 出现前我们通常做法:写大段 prompt、写 subagent 描述、或准备脚本文件放目录里、再通过 @ 命令或特定方式触发让模型或 Claude Code Agent 框架加载。

本质都是在手动模拟Agent能力的模块化和按需加载

现在有 Skill,通过标准化机制,可在合适时间把真正需要能力加载进 Agent。

从这个角度看,Skill 把原本巨大复杂 CLAUDE.md(System Prompt)拆解成按需使用小模块

模型的上下文是有限的, 不可能把所有东西一次性塞进上下文来强化模型, 需要精耕细作。

举一个实际案例:同事需要帮客户通过 Claude Code 帮写内部框架代码,但这框架是完全私有实现。

Claude Code 当然不知框架怎么写,于是他们把大量内容——开发文档、编码规范、架构说明、校验方式、编译流程——全塞进很长 CLAUDE.md 文件里,让 Claude Code 启动时一次性加载。

这方式效率很低:文档多、规则杂、且所有内容常驻上下文。甚至还会尝试通过 MCP 加载外部代码文件,但本质问。

Skill 的模块化优势

现在有 Skill 就完全不同。可把开发文档按场景拆分多个 Skill。比如:一个 Skill 负责 JWT 相关实现、一个 Skill 负责定制 Spring Boot 应用开发规范。

需开发某类应用时,只加载对应 Skill。

更进一步,Skill 间还可相互提示:什么情况用其他 Skill, 甚至自动安装skill。

实际是给模型提供更高质量”暗示”,让它自发地不断扩展后续执行中能力边界。

动态文档加载的可能性

我第一次看到 Skill 时,最直接的感受是:开发文档终于可动态加载了.

 不需要通过 MCP Server 或者 explore task agent 去检索文档。完全可通过 Skill 方式加载内部文档。

而且,如文档比较多需复杂的检索条件,甚至可在 Skill 里提供命令行工具:命令行背后也可能是其他 agent (比如 Codex!)、把用户 prompt 转文档查询、再返最相关内容。

过程中,Claude Code 可能还调其他模型或工具。

通过 Skill,可**不断扩展系统能力和边界**。

Shell + Skill 的自然范式

最近经常看到 Flash 作者关于 AI 的分享, 我非常赞同他的其中一个观点 (https://lucumr.pocoo.org/2025/11/21/agents-are-hard)

「如果你根本不需要 MCP 呢?」很多 MCP server 過度設計,塞了一堆工具吃掉大量 context,其實用簡單的 CLI 工具透過 Bash 執行就好。
引用自 https://ihower.tw/blog/13513-agent-design-is-still-hard-2025

Agent 不一定非要学复杂新协议。有 bash 就可做几乎任何事。
而且已有大量成熟工具:GitHub 相关工具、rg 文本搜索工具、各种服务命令行封装。这些工具本身为程序员设计。

为什么选择 Shell + Skill

原因几个:

  1. 按需加载:用到才加载,不污染上下文
  2. 自描述:需时直接 --help 获取说明
  3. 错误信息友好:Shell 工具错误输出给人看,通常是文本+错误码,而非冗长异常堆栈

从 Agent 角度看,这点很重要。Agent 就是在”扮演人”,而 Shell 工具本为人设计。

而 Shell 工具的优势就是:

  • 交互简洁
  • 组合使用

虽然 shell 工具的参数很多, 对不熟悉工具的人来说, 学习成本可能略高,但 Agent 学习速度远快于人, 它可以通过帮助文件瞬间完成学习。

所以, 引入 Skill + Bash + Tool 的组合能力之后,Agent 能力会非常强大。

Skill 设计的深层思考

我认为,Skill 是以小见大设计

表面只是简单能力:

动态读取 Markdown 文件

但相比之下:Sub Agent 更偏向具体执行、MCP 是固化远程调用必须全量加载。

而 Skill 更像给模型注入知识能力,让它在合适场景下执行具体事情。

甚至可把 MCP 拆解多个 Skill:不同场景只暴露必要接口、其他无关接口不加载。

这样一来:不需要 MCP 代理、本地就可完成能力分发。扩展性会很强。

这也让我强烈感觉:在模型驱动的时代,我们不应过度依赖强约束或复杂的强协议。.

它们很快就会限制住模型的能力, 然后马上被抛弃。

只要协议本身具备足够好扩展性,再配合模型能力和适度约束,系统可扩展性反而更强。

前言

在文章 [1]和 文章 [2] 中, 分别介绍了 bootloader 和 qmk 核心功能的移植.

这篇文章会稍微深入一些, 介绍自己一步一步了解和熟悉 stm32f103 的过程, 摸索之后配置好了键盘更多功能.

这篇文章中, 将通过阅读相关 datasheet, 了解 stm32 的基础知识, 给固件加上更多功能:

  • 外置持久化存储 i2c eeprom
  • 基于 dma 和 pwm 的 RGB Matrix.

成功点亮RGB

源码仓库 https://github.com/hitsmaxft/qmk-kb-bheznz

Read more »

前言

连接效果

在前文中, 我介绍了移植野火开发板 stm32f103开发板的 dfu bootloader, 接下来要介绍的是如何迁移qmk到开发板上.
这篇文章会涉及修改 qmk 源码,添加 stm32f103 键盘, 配置测试小键盘的布局, 最后通过 stm32duino 的 dfu 模式刷入固件.
一同解释 qmk 的一些基础知识.
后面的文章逐步深入到 RGB 和存储等.

源码仓库 https://github.com/hitsmaxft/qmk-kb-bheznz , 最新的代码在我的qmk-firmware 中, 这一份是手工尽量同步.

Read more »

为啥有这篇文章

最近我业余时间给自己的键盘刷上了 qmk 固件, 其中主控代码和 RGB 跑起来了, 但是不太理解硬件原理, 于是想到买了几年一直吃灰的野火开发版, 正好可以用来了解stm32的开发细节。

这里根据开发板的资料,一步步搭建起了专用的 qmk 固件.

这是第一篇文章,讲讲给开发板加上 bootloader。

接下来的文章内容中, 将一步一步讲解给指南者开发板修改了编译 stm32duino-bootloader 用于实现用户态 DFU 功能。

Read more »

前言

gemini-cli 的基础功能已经完备, 我就计划着把包发布到 pypi 上去, 这样子公司那边开发的工具也可以直接引用, 避免和工作的代码产生直接交集.

python 官方提供了很完善的 github action 发布和自动生成 release 的教程, 但它是基于 pip 和 build 写的, 用 poetry 实现构建依赖管理的项目就需要不少改动了.

而我是直接从 github 官方的 python 发布 workflow 修改而来, 只实现了构建和发布, release 以后再完善了.

需要以下几个步骤

  • 配置 workflow 文件
  • 配置仓库的发布 密钥
  • 在包的发布页面授权github workflow
Read more »

OrbStack 提供了 Linux Machine 功能, 可以在 mac os 上得 WSL2 一样的体验.

由于众所周知的原因, 我需要在宿主机上开了 clash 作为代理工具. 在本地的 7890 端口上提供 http 代理, 但没有开全局代理. 只有 firefox 和 github clone 会进过 7890

但这也就导致了 OrbStack 里的 linux 默认是不经过代理.
这里我想要的效果是 linux machine 全走代理. 宿主机还是保持按需使用.

Read more »

前言

由于前阵子 MacBookPro 重新格式化了, 于是用 nix-darwinhome-manager 重新构建了自己的配置管理. 这里写一篇文章记录一些个人对 nix 的学习和总结

Read more »

前言

由 chatgpt 生成…

在当今的技术领域,配置管理变得越来越重要。对于开发者和技术爱好者来说,保持一致的开发环境和工具配置是至关重要的。然而,传统的配置文件管理方式可能会导致配置分散、混乱和难以维护的问题。

幸运的是,有一个强大的工具可以帮助我们解决这些问题,那就是home-manager。Home-manager是一个基于Nix的工具,旨在帮助用户统一管理他们的配置文件,并通过声明性的方式进行配置。

本文将介绍如何安装home-manager以及使用它来管理一些常见的配置文件,例如zsh、bash和vim。我们将探索如何使用home-manager的内置功能和插件来简化配置文件的管理,并介绍如何定制和贡献新的配置。

接下来的内容中, 主要围绕两个工具的使用.

  • home-manager 声明式, 函数式, 面向终态的用户配置管理方案
  • 通过 yadm 完成将配置文件同步到 github
Read more »

rock960 是一块不太常见的 96boards 规范得的 rk3399 单板,目前厂家已经不维护了(email咨询得知).

不过还好他们邮件给了可以下载最新固件的地址 sd-card-images , 下载下来发现是 sd card 用的镜像, 所以需要这里说下怎么制作emmc 版本镜像

还需要稍微处理以下就可以得到更新的 debian 固件

接下来就介绍一下, 从原始的 96boards 镜像上更新 rootfs 为更新的版本的 debian 系统. 这样子从老的 boot 分区启动, 然后引导最新的Debian.
最完美的方式, 当然是boot分区也一起升级了, 这个以后再折腾了.

Read more »

总算来谈下这个烂大街的话题了。

面向对象一般会谈继承封装多态,但列举一门理论具备什么特征并不能帮助程序员合理地应用这门理论。

在传统面向数据和过程的编程方式中, 我们关心的东西有

  • 特定结构的数据
  • 针对数据定义的行为
1
2
struct {} A;
void function act(A a);

实话说,这样的编程手段对于解决问题本身已经够用了。

但对于现代化的软件工程项目,为了更好地解决开发效率与开发成本等问题,
我们需要引入一种更加有效的描述方式。使得开发任务更加顺畅, 开发成本能够有效降低。

java 界似乎在远古时期宣扬了一种基于 class 的编程方式,并将这种行为称作 oop 的有效实践。
而我们知道, 那端时期 java 界所宣扬的从来就没几个是靠谱的。

首先要摒弃所谓的 class 方式就是面向对象。
即使是 javascript 这种基于原型的编程语言,也是满足面向对象原则的。

只是纯粹描述数据本身的东西, 我们称之为 struct(结构体), 它具备了类型和实例两个内容。
而面向对象,则需要在结构体之上,增加一个可以描述行为的方式, 这种方式一般叫做 method (方法)
这里方法虽然看起来和 function 类似, 但是它是提供了数据绑定的, 而函数只和输入输出相关,不关心状态。

这个东西从实现上,仅仅是个手法而已,和上文所提到的结构体和函数的接合可以达到同样的目的。
现代化的编程语言,正是通过这样的变换手法,优化编程的模型而提高生产力的。

使用面向对象方式编程的开发者,关注的是某一个对象,它所具备的行为(method)。

好吧,因为类型的扩散,我们还是得考虑多种类型之间的交互, 和前面说的 函数 + 结构体 的方式,
差别并不大。
为了优化这中情况,程序员能做到的无非就是简化传入参数中的类型,尽量使用平坦的数据类型.

1
2
3
4
Name n = new Name("John");
Person p = new Person();
p.setName(n);
p.sayName();

上面的这个例子里, 我们倾向于认为 Name 是一个简单的类型, 而 Person 则是一种具有复杂行为的
高级类型。
但是,也正是面向对象语言提供的便捷手段,可以通过 封装 手段解决这个问题。

1
2
3
//JhonPerson extends Person
Person jp = new JhonPerson();
jp.sayName();

面向对象的本质在于 消息传递, 方法调用 不过是类 java 语言中的一种实现。
为了简化开发任务, 通过扩展组合等手段, 可以轻易地将复杂行为封装到新类型中,但是提供了相同的接口。

这个例子中, 我们可以体验到面向对象编程中的封装和多态.
封装重用了数据, 而多态重用了行为.

面向对象语言过度地提到继承, 似乎所有关联任务都可以通过 extends 来解决。
而我们知道,对象之间的关联关系, 扩展基类意味着需要继承行为和数据,是一种垂直的继承。
为了打破这种限制, 提供能够横向扩展的能力, java 引入了 接口 ,设计模式则提到了 组合.

前面提到,方法是一种和类型绑定的行为,如果只是垂直的单继承体系,多态只能在继承类型之间实现。
为了能够满足不同类型之间能够完善地支持多态,接口的引入解决了这个问题,但是,事情并没有能够完美地结束。

java 和其他类似语言中,由于函数并不是语言中的第一公民,接口仍然是一个具体的类型,只不过进行了一次间接的多继承.
到这里, 我们还是没能够得到我们希望的, 完全基于行为的多态。

在动态语言中,方法的调用是动态进行的,近似于在运行时直接查找同名方法,因此不存在这个问题。
而在静态语言中实现了类似行为的有 go , 虽然最终因为缺少泛型而导致了 interface {} 满天飞。

好不容易脱离了类型和方法的限制,又回到了如何解决方法和参数类型之间的绑定关系.

0%