阅读视图

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

AI Mind v0.0.8:从单 Skill 到多 Skill,我如何让第二个 Skill 真正成立

本文对应项目版本:v0.0.8

v0.0.7 里,我已经给 AI Mind 落下了第一个正式 Skill:utility-skill

那一版的重点,是证明一件事:

在 Multi-Tool Runtime 之上,是否真的能再长出一层更稳定的能力模式。

但只有一个 Skill,其实还不够。

因为单 Skill 最多只能证明:

  • 这套结构能跑
  • Runtime 能感知 Skill
  • Tool 可以被 Skill 收口

它还证明不了另一件更关键的事:

当系统开始进入多 Skill 阶段时,这层抽象到底是不是真的成立。

所以到了 v0.0.8,我真正要回答的问题就变成了:

  1. 第二个 Skill 应该是什么
  2. 什么样的 Skill 才值得进入正式版本
  3. 多 Skill Runtime 的边界应该怎么收
  4. 前端又该怎么把这种能力模式切换表达出来

这篇文章想讲的,就是我如何从最初的 writer-skill 设想,最后收敛到了 reader-skill,并让 AI Mind 真正迈出“从单 Skill 到多 Skill”的第一步。

skill-1.gif

为什么多 Skill 是这一版必须面对的问题

如果项目一直只有一个 utility-skill,那 Skill Runtime 很容易停留在一个比较尴尬的状态:

  • 看起来像是做出了一层新抽象
  • 但又很难证明它不是一次性的特殊 case

因为只有一个 Skill 时,你很难回答这些问题:

  • Skill 之间的边界能不能真正拉开
  • Runtime 是否能根据不同 Skill 暴露不同 Tool 子集
  • 自动模式下的路由是否还有意义
  • 前端是否需要为不同 Skill 提供更明确的交互入口

换句话说,单 Skill 更像是在证明“这套机制存在”,而多 Skill 才开始证明“这套机制成立”。

所以 v0.0.8 的重点,不是再多做一个功能,而是:

让第二个 Skill 真正成为一个有独立边界、有独立 Tool 价值的能力模式。

为什么最开始想到的是 writer-skill

一开始我最自然想到的第二个 Skill,其实是 writer-skill

这个方向表面上看很合理:

  • 它和 utility-skill 差异足够大
  • 用户很容易理解“写作模式”
  • 前端做模式切换时,也很容易有感知

所以我最初尝试的方向是:

  • 做一个 writer-skill
  • 再配一个偏结构整理的 Tool
  • 让模型在“改写、总结、整理、生成标题”这类任务上走另一条路径

从想法上说,这条线没有问题。

真正的问题出在落地后。

很快我就发现,写作整理类任务和 utility-skill 最大的不同在于:

它们里有很大一部分,其实本来就是大模型原生就会做的事情。

比如:

  • 润色一段话
  • 把几句话改写得更自然
  • 整理成一段通顺表达
  • 概括几个点

这些任务里,模型往往会直接回答,而不是老老实实触发 Tool。

也就是说,writer-skill 可以命中,但 Tool 的独特价值却不够稳定。

为什么我最后放弃了 writer-skill

最后让我决定止损的,不是某一个 bug,而是一个越来越明确的判断:

第二个正式 Skill,最好补的是模型没有的能力,而不是模型已经比较擅长的能力。

writer-skill 的问题主要有三个。

1. 写作整理很多时候是模型原生能力

写作并不是不能做成 Skill,而是它很难在当前阶段承担“证明多 Skill Runtime 成立”的任务。

因为一旦用户的需求是:

  • 改写
  • 润色
  • 概括
  • 整理成更自然的一段话

模型会天然倾向于自己直接写。

这意味着:

  • Skill 也许命中了
  • 但 Tool 不一定会稳定触发

2. Tool 没形成“非它不可”的能力差

如果一个 Tool 提供的只是:

  • 换个结构
  • 换个格式
  • 帮你整理一下语序

那它很容易被模型直接绕过去。

因为模型会判断:

我自己直接写一段,往往比调用一个结构整理 Tool 更简单。

这和 calculatordatetimeunit-convert 完全不一样。

后者一旦不用 Tool,模型就很容易答错;而前者即使不用 Tool,模型也很可能还能答得不错。

3. 第二个 Skill 不应该只是“多一种说话风格”

这是这轮最重要的取舍。

我最后越来越明确地意识到:

多 Skill 的关键,不是“多几个模式名字”,也不是“多几段 Prompt”。

真正值得留下来的 Skill,应该满足至少一条:

  • 它组织了一组边界清晰的 Tool
  • 它补的是模型原本拿不到的能力
  • 它能明显改变 Runtime 的可用能力范围

writer-skill 在当前阶段没有足够强地满足这些条件。

所以我最后放弃它,并不是因为写作不重要,而是因为它不适合当前作为“第二个正式 Skill”。

为什么第二个 Skill 最终变成了 reader-skill

我最后把第二个 Skill 改成了 reader-skill

因为这时我更想验证的是:

Skill Runtime 是否能承载一类模型本身完全拿不到的信息能力。

reader-skill 对应的正是这类场景:

  • 实时天气
  • 本地文本文件

它们有一个共同点:

没有 Tool,就没有能力来源。

这和写作场景最大的区别在于:

  • 没有天气 Tool,模型就拿不到实时天气
  • 没有本地文件读取 Tool,模型就看不到你的项目文件

这时候 Tool 不再是“可选增强”,而是“能力成立的前提”。

于是 reader-skill 的价值就变得非常明确:

  • 它不是在给模型增加一种风格
  • 而是在给模型接入一类新的上下文来源

这就让第二个 Skill 终于拥有了足够清晰的独立边界。

这版 reader-skill 是怎么收边界的

reader-skill 这一版我只落了两个 Tool,而且每个 Tool 都故意收得很小。

1. city-weather

用途非常单一:

  • 查询指定城市的实时天气

它只收一个参数:

  • city

数据源我也没有做得很重,而是直接用了轻量的 wttr.in

这背后的考虑很简单:

  • 这版的重点不是做一个完整天气系统
  • 而是验证“实时信息如何进入 Skill Runtime”

也就是说,city-weather 的价值不在于“做得多强”,而在于它非常直接地证明了:

没有外部 Tool,模型就是拿不到这部分实时信息。

2. local-text-read

local-text-read 同样只做一件事:

  • 读取项目根目录下的直接文本文件

它也只收一个参数:

  • filename

而且我给它加了很强的边界限制:

  • 只允许根目录直接文件
  • 不允许子目录
  • 不允许绝对路径
  • 不允许 ../
  • 只允许文本类文件

这也是我这一版很看重的一点:

Tool 的价值不只是“能做什么”,还包括“它不会越界做什么”。

如果第二个 Skill 要证明它是一种正式能力模式,那它不仅要有能力来源,也要有稳定边界。

多 Skill Runtime 这一版真正收敛了什么

v0.0.8,项目里的 Skill 边界终于开始变清楚了。

现在可以比较明确地把它们分成两类:

utility-skill

负责确定性实用任务:

  • 计算
  • 时间日期
  • 单位换算
  • 文本转换

对应 Tool:

  • calculator
  • datetime
  • unit-convert
  • text-transform

reader-skill

负责外部上下文获取:

  • 实时天气
  • 本地文件读取

对应 Tool:

  • city-weather
  • local-text-read

这时候 Skill 才真正不再只是“一个标签”,而是:

  • 当前属于哪一种能力模式
  • 当前允许模型使用哪些 Tool
  • 当前回答主要建立在哪一类能力来源上

多 Skill 链路图

flowchart LR
    A["用户请求"] --> B["/api/chat"]
    B --> C{"是否显式传入 skill"}
    C -- "是" --> D["直接命中对应 Skill"]
    C -- "否" --> E["轻量规则路由"]
    E --> F{"命中 utility / reader ?"}
    F -- "utility" --> G["utility-skill"]
    F -- "reader" --> H["reader-skill"]
    F -- "未命中" --> I["普通聊天链路"]
    G --> J["allowedTools 过滤 ToolRegistry"]
    H --> J
    J --> K["模型在当前 Tool 子集里决定是否调用 Tool"]
    K --> L["Runtime 执行 Tool"]
    L --> M["流式返回 reasoning / tool / text"]

这一版里我刻意没有做的,是:

  • 模型自主 Skill 路由
  • 多 Skill 编排
  • 更复杂的 Agent 化链路

因为我想先证明的不是“系统越来越聪明”,而是:

多 Skill Runtime 在结构上已经开始稳定成立。

为什么前端也要一起进入正式组件基线

这版还有一个我认为非常值得一起写进去的变化:

前端开始正式进入 shadcn/ui 基线阶段。

原因其实也很现实。

当输入区开始同时出现:

  • 模型选择器
  • Skill 模式切换
  • 深度思考开关
  • 推理过程面板
  • Tool 卡片

如果继续完全靠手写样式往前堆,界面会越来越像几套东西拼在一起。

所以这一版我顺手做了前端统一收口:

  • 正式接入 shadcn/ui
  • 使用 Radix
  • 图标统一为 lucide-react
  • 主题走 cssVariables
  • 当前基线切到 radix-vega

这一轮已经统一下来的区域包括:

  • 输入区控制条
  • 顶部错误条
  • 推理过程面板
  • Tool 卡片
  • 空状态

而且我还补了几项比较细的交互收口:

  • 输入框上下边距收紧
  • 推理面板上下边距收紧
  • Tool 状态色区分:
    • 完成:绿
    • 执行中:蓝
    • 失败:红
  • 实用读取 模式下的提示文案分开

这一点对我来说很重要,因为它说明:

多 Skill Runtime 的成立,不只是后端 Runtime 的问题,也是前端表达能力的一部分。

skill-2.gif

这版最重要的工程结论

如果要我用几句话总结 v0.0.8,我最想留下的是这三点:

1. 不是所有 Skill 都值得进入正式版本

有些 Skill 看起来方向对,但它不一定适合当前版本的验证目标。

writer-skill 就属于这种情况:

  • 它不是完全没价值
  • 但它不适合当前承担“证明第二个 Skill 成立”的任务

2. 第二个 Skill 最好补的是模型缺失的能力

如果 Tool 补的是模型原本就会做的事情,那它就很容易被绕过。

但如果 Tool 补的是模型完全拿不到的上下文,那 Skill 的价值会立刻清晰很多。

这也是为什么 reader-skillwriter-skill 更适合当前阶段。

3. 多 Skill 的成立,不只是 Runtime 的事,也是 UI 的事

一旦系统开始真正区分:

  • 自动 / 实用 / 读取
  • reasoning / tool / text
  • 不同 Tool 状态

那前端也必须同步给出更统一、更稳定的表达方式。

这也是为什么这版里,我没有把 UI 统一看成“顺手做的样式活”。

它其实也是版本收敛的一部分。

这一版之后,我更清楚了一件事

如果说 v0.0.7 证明的是:

Tool Runtime 之上可以长出第一层 Skill Runtime。

那么 v0.0.8 证明的就是:

多 Skill 不是多几个不同名字的 Prompt,而是第二个 Skill 是否真的打开了一块新的能力边界。

对现在的 AI Mind 来说,这块边界已经开始变得清楚:

  • utility-skill:确定性实用任务
  • reader-skill:外部上下文获取

这也让整个 Runtime Skeleton 比之前更像一个会继续长大的系统,而不是一组不断堆叠的局部功能。

后面会往哪走

如果继续沿这条线往后走,我更关心的是:

  • reader-skill 的稳定性继续收口
  • 网页读取 / MCP 能力怎么接入
  • 更高层的 Agent Runtime 什么时候开始真正有必要

但至少在 v0.0.8 这个点上,我已经比较确认:

第二个 Skill 终于不是一个“看起来像 Skill 的名字”,而是一块真正成立的能力模式。

最后

这个项目还会继续沿着:

  • reader-skill 稳定性收口
  • 网页读取 / MCP
  • Agent Runtime

这些方向继续往前走。

如果这篇文章对你有帮助,欢迎到 GitHub 看看项目,也欢迎顺手点个 Star。

仓库地址: github.com/HWYD/ai-min…

我是怎么把 Multi-Tool Runtime 升级成第一层 Skill Runtime 的

本文对应项目版本:v0.0.7

v0.0.6 里,我已经把项目从单 Tool Calling 推进到了 Multi-Tool Runtime:

  • calculator
  • datetime
  • text-transform
  • Tool Registry
  • 前端多 Tool 卡片展示
  • 最近 N=8 轮上下文窗口

走到这一步后,我发现项目开始进入另一个更像工程问题的阶段:

当系统里已经有多个 Tool 之后,下一步到底应该继续堆 Tool,还是先往上抽一层更稳定的能力封装?

这就是 v0.0.7 想回答的问题。

这一版我没有直接去做 Agent,也没有急着接 MCP,而是先做了一件更克制、但我认为更关键的事:

在现有的 Multi-Tool Runtime 之上,长出第一层 Skill Runtime。

这篇文章主要讲 4 件事:

  1. 为什么现在做 Skill 是合适的
  2. utility-skill 到底解决了什么问题
  3. Runtime 怎么接 Skill,而不是把它做成另一套散乱逻辑
  4. 为什么 Prompt 很重要,但版本主题仍然要保持克制

项目主界面截图

skill-2.png

Skill Runtime 链路图

用户输入
  -> /api/chat
    -> 读取 options.skill
      -> SkillRegistry 获取 skill 定义
        -> allowedTools 过滤 ToolRegistry
        -> 注入 skill.systemPrompt
          -> 模型 planning
            -> 选择 tool_call
              -> Runtime 校验/执行 tool
                -> tool result 回填
                  -> 最终回答流式返回前端

为什么 v0.0.7 不直接做 Agent

这件事必须先讲清楚。

因为从版本节奏上看,v0.0.6 做完之后,很容易产生一种冲动:

  • 既然已经有多个 Tool 了,是不是下一步就该直接做 Agent?
  • 是不是该把 Skill、MCP、记忆、任务规划一起拉进来?

我最后没有这么做,原因其实很简单:

  • v0.0.5 验证的是:单 Tool Calling 可不可行
  • v0.0.6 验证的是:Multi-Tool Runtime 能不能站住
  • 到了 v0.0.7,更值得验证的是:Tool 之上能不能再稳定长出一层能力模式

如果这时直接跳去做 Agent,会把很多变量混在一起:

  • Tool 选择是否稳定
  • Tool Runtime 是否清晰
  • Prompt 约束是否足够
  • 是否需要多步计划
  • 是否需要记忆系统
  • 是否需要外部能力接入

这些问题一旦一起出现,版本主题很容易被冲散。

所以我给 v0.0.7 定下的边界非常明确:

  1. 只落一个正式 Skill:utility-skill
  2. 新增一个 Tool:unit-convert
  3. 不做 Skill UI
  4. 不做自动 Skill 路由
  5. 不做 Agent Loop
  6. 不做 MCP 接入

一句话概括:

这一版不是为了证明“系统更聪明了”,而是为了验证:Tool Runtime 之上,能不能再稳定长出第一层 Skill Runtime。

这版到底解决了什么问题

v0.0.6 做完之后,项目已经有了多个 Tool,但也暴露了一个很真实的问题:

1. Tool 还是“散着的能力”

虽然已经有:

  • calculator
  • datetime
  • text-transform

但它们本质上还是一组分散的原子能力。系统并没有一层更高的语义去表达:

  • 当前是在什么任务模式下工作
  • 当前允许使用哪些 Tool
  • 当前回答应该更偏“结果优先”还是“解释优先”

2. 多 Tool 之后,Prompt 开始越来越重要

比如下面这些问题:

  • 357×28+999+1 等于多少
  • 今天是周几
  • 提取这段文本里的链接

按理说都应该优先走 Tool。但如果没有一层更清晰的能力模式约束,模型很容易出现这些行为:

  • 自己先口算
  • reasoning 里说“应该调用工具”,但没真正发起 tool_call
  • 输出风格越来越散

3. 我需要一层更高的“能力模式”

如果继续在 chat-service.ts 里堆更多 Tool 特判,最后只会让 Runtime 越来越重。

真正需要的,是一层更高的东西,让系统能够明确知道:

  • 当前属于哪类能力域
  • 当前允许哪些 Tool
  • 当前输出应该是什么风格

这个东西,就是这一版里的 Skill。

Skill 在这里到底是什么

我对 Skill 的理解,不是“另一个 Tool”,也不是“精简版 Agent”。

它更像是:

站在 Tool 之上的一层高阶能力模式。

v0.0.7 里,我刻意让 Skill 只承担很克制的职责:

  • 定义一个任务域
  • 提供一段 system prompt
  • 限制当前允许使用的 Tool 集
  • 约束输出策略

不直接执行 Tool,也不负责多步规划

这点很重要,因为我不想让 Skill 偷偷长成 Agent。

为什么是 utility-skill

第一版 Skill 我没有做 research-skill,也没有做 writer-skill,而是选了一个更克制的方向:

  • utility-skill

它对应的是一类非常具体的任务:

  • 精确计算
  • 时间与日期处理
  • 文本转换与提取
  • 单位换算

我选它主要有三个原因。

1. 它和当前 Tool 集天然衔接

v0.0.6 已经有:

  • calculator
  • datetime
  • text-transform

这些 Tool 天然就属于“日常实用任务”的一部分。

所以 utility-skill 不是硬造出来的抽象,而是从当前 Tool 集里自然长出来的。

2. 它足够轻,但足够证明 Skill 是成立的

它不需要:

  • 外部系统
  • MCP
  • 复杂记忆
  • Agent Loop

但它足够证明一件很重要的事:

Skill 不是一段 Prompt,而是一层真的会影响 Runtime 的能力定义。

3. 它能继续长,但不会把版本做散

如果第一版就做 research-skill,很快就会牵扯到:

  • 搜索
  • 抓取
  • 来源引用
  • 多步编排

utility-skill 足够小,刚好能帮我验证 Skill Runtime 的骨架,不会把版本主题拉散。

总体架构:在 Multi-Tool Runtime 之上再加一层 Skill

这一版的主链路长这样:

用户输入
  -> 前端页面
    -> /api/chat
      -> chat-service
        -> Skill Registry
        -> Tool Registry
        -> ChatOllama
          -> tool calling / tool execution
            -> NDJSON stream
              -> useChatStream
                -> reasoning / tool / text 渲染

Skill Runtime 总体链路图

flowchart LR
    A["用户请求"] --> B["/api/chat"]
    B --> C["命中 Skill"]
    C --> D["读取 SkillDefinition"]
    D --> E["限制可用 Tools"]
    E --> F["模型选择是否调用 Tool"]
    F --> G["Runtime 执行 Tool"]
    G --> H["组合最终结果"]
    H --> I["流式返回前端"]

如果用一句话概括,就是:

Tool 负责原子能力,Skill 负责高层能力模式,Runtime 负责把两者接起来。

这一版里最关键的变化不是“多了一个 unit-convert”,而是:

  • Runtime 先感知 Skill
  • 再决定当前可用 Tool 集
  • 再把这套能力边界交给模型

Tool 之上再加一层 Skill,最重要的是 Registry

如果想让 Skill 成为正式能力层,第一步就不能继续把相关逻辑散落在 chat-service.ts 里。

所以我先做的是:把 Skill 和 Tool 一样,也收进 Registry。

关键代码:Skill 的统一定义接口

这段代码的作用是:让 Skill 也成为一个可注册、可查询、可扩展的能力单元。

export type SkillOutputPolicy = 'concise-utility'
export type SkillResultPolicy = 'tool-first'

export interface SkillDefinition {
  name: string
  description: string
  systemPrompt: string
  allowedTools: string[]
  outputPolicy?: SkillOutputPolicy
  resultPolicy?: SkillResultPolicy
  routingHints?: string[]
  isAvailable?: () => boolean
}

这里我最看重的,不是 namesystemPrompt,而是下面这些字段:

  • allowedTools
  • outputPolicy
  • resultPolicy
  • routingHints

它们决定了 Skill 不是“模型的一段额外说明”,而是 Runtime 可以真正感知的一层能力配置。

关键代码:Skill Registry

这段代码的作用是:让 Runtime 只通过统一入口读取 Skill,而不是在主流程里到处判断具体 Skill 名称。

export interface ChatSkillRegistry {
  list(): SkillDefinition[]
  listActive(): SkillDefinition[]
  get(name: string): SkillDefinition | undefined
}

export function createChatSkillRegistry(skillDefinitions: SkillDefinition[]): ChatSkillRegistry {
  const skillDefinitionMap = new Map(
    skillDefinitions.map(skillDefinition => [skillDefinition.name, skillDefinition])
  )

  return {
    list() {
      return skillDefinitions
    },
    listActive() {
      return skillDefinitions.filter(skillDefinition => skillDefinition.isAvailable?.() ?? true)
    },
    get(name: string) {
      return skillDefinitionMap.get(name)
    },
  }
}

这一步其实是在为后面的演进打基础:

  • 今天是 utility-skill
  • 后面可以是 research-skill
  • 再往后甚至可以有 MCP-based skill

但 Runtime 主链不需要被这些具体名字污染。

utility-skill 是怎么定义的

这一版里,utility-skill 并没有做什么“神秘编排”,它做的事情非常务实:

  • 定义任务域
  • 指定允许使用的 Tool
  • 用 Prompt 约束输出风格

关键代码:utility-skill definition

这段代码的作用是:把“日常实用任务”正式定义成一层 Skill。

export const utilitySkillDefinition: SkillDefinition = {
  name: 'utility-skill',
  description: '处理日常确定性实用任务的稳定能力层',
  systemPrompt: `...`,
  allowedTools: ['calculator', 'datetime', 'text-transform', 'unit-convert'],
  outputPolicy: 'concise-utility',
  resultPolicy: 'tool-first',
  routingHints: [
    'math',
    'date',
    'time',
    'weekday',
    'relative-date',
    'convert',
    'markdown-to-text',
    'extract-links',
    'json-format',
    'unit-conversion',
  ],
}

这里最关键的有两个点。

1. allowedTools

这一版里 Skill 的价值不只是“多一段 Prompt”,而是它会真的影响当前 Runtime 的可用能力边界。

也就是说,在 utility-skill 下,当前允许给模型看到的 Tool,就是这四个:

  • calculator
  • datetime
  • text-transform
  • unit-convert

2. tool-first

我给 utility-skill 选的结果策略是:

  • tool-first

原因很简单,这类任务本来就是高度确定性的:

  • 计算
  • 日期
  • 单位换算

如果模型已经拿到了 Tool 结果,再让它自由发挥,反而更容易把答案写歪。

Runtime 怎么真正接 Skill

Skill 真正有价值的地方,不在 definition 文件,而在于 Runtime 会不会把它接进去。

关键代码:请求里读取 Skill

这段代码的作用是:让 Skill 成为正式请求参数,而不是隐含状态。

function resolveRequestedSkill(request: ChatRequest): SkillDefinition | undefined {
  const skillName = request.options?.skill?.trim()

  if (!skillName) {
    return undefined
  }

  const skillDefinition = getChatSkillDefinition(skillName)

  if (!skillDefinition || !(skillDefinition.isAvailable?.() ?? true)) {
    throw createInvalidSkillError(skillName)
  }

  return skillDefinition
}

这里我刻意让 skilloptions.skill 的方式显式传入,而不是让系统自动猜当前该用哪个 Skill。

这是一个刻意的版本边界控制:

  • 先验证 Skill Runtime 本身
  • 暂时不把“Skill 路由”这个变量引进来

关键代码:按 Skill 过滤当前 Tool 集

这段代码的作用是:让 Skill 真实改变当前 Runtime 的可用能力边界。

function getActiveToolDefinitions(skillDefinition?: SkillDefinition): ChatToolDefinition[] {
  const activeToolDefinitions = chatToolRegistry.listActive()

  if (!skillDefinition) {
    return activeToolDefinitions
  }

  const allowedToolNames = new Set(skillDefinition.allowedTools)

  return activeToolDefinitions.filter(toolDefinition => allowedToolNames.has(toolDefinition.name))
}

这一步非常关键,因为它说明:

  • Skill 不是文本层能力
  • Skill 是 Runtime 层能力

后面如果你要接 MCP、接更多 Skill,这种边界会非常有价值。

为什么还要新加 unit-convert

如果这一版只做 utility-skill,而不再新增任何 Tool,那它看起来会很像:

  • 把现有 Tool 打包进一个 Skill

这会让 Skill 的存在感偏弱。

所以我在 v0.0.7 里又补了一个很合适的新 Tool:

  • unit-convert

它的价值在于:

  • calculator / datetime / text-transform 一样,都是确定性任务
  • 非常贴近日常实用场景
  • 能让 utility-skill 更像一个完整的实用能力包

关键代码:unit-convert 的 schema

这段代码的作用是:让单位换算成为一个边界明确、可校验、可扩展的 Tool。

const unitConvertToolSchema = z.object({
  value: z.number().finite(),
  from: z.enum(supportedUnits),
  to: z.enum(supportedUnits),
})

这一版我刻意把范围控制得很小,只支持:

  • 长度
  • 重量
  • 温度

没有去碰:

  • 货币汇率
  • 存储单位
  • 实时换算

因为 v0.0.7 的重点不是“功能越多越好”,而是“Skill Runtime 是否站得住”。

关键代码:unit-convert 的 definition

这段代码的作用是:unit-convert 纳入统一 Tool Runtime,并声明它的结果是权威结果。

export const unitConvertToolDefinition: ChatToolDefinition<z.infer<typeof unitConvertToolSchema>> = {
  name: 'unit-convert',
  tool: unitConvertTool,
  schema: unitConvertToolSchema,
  normalizeArgs: normalizeUnitConvertToolArgs,
  formatInput: formatUnitConvertToolInput,
  getDisplayConfig: args => ({
    title: 'unit-convert',
    action: 'convert',
    inputPreview: formatUnitConvertToolInput(args),
  }),
  resultIsAuthoritative: true,
}

这里的 resultIsAuthoritative = true 很重要。

因为单位换算和计算题一样:

Tool 结果应该被视为权威事实,而不是让模型再自由改写。

这一版最真实的坑:Prompt 很重要,但版本主题也要克制

如果只看实现结构,v0.0.7 好像已经很完整了。

但真正做下来之后,我觉得最值得写的,反而是一个很现实的工程结论:

Prompt 很重要,但版本不能为了追求“更稳”就把一切都做成特判。

datetime 这种时间类问题上,我确实做过多轮 Prompt 收紧尝试,比如:

  • 明确要求时间、日期、星期问题优先使用 datetime
  • 禁止模型在 reasoning 里只说“应该调用工具”却不真正发起 tool_call

这些约束是有价值的,但我最后没有把这版写成“到处加特定问题兜底”的版本。

原因是 v0.0.7 的主题是:

  • 验证 Skill Runtime 是否成立

而不是:

  • 把所有边界问题都靠局部补丁兜住

这也是为什么当前版本保留的是:

  • Skill Registry
  • allowedTools
  • 统一 prompt 约束
  • Runtime 主链校验与错误透传

而不是把每一个相对日期表达都变成运行时特判。

前端这版最大的变化:现在渲染的是 Skill 下的 Tool 事件流

前端这版没有做新的 Skill UI,这是我故意的。

因为 v0.0.7 的重点不在“多一个标签”,而在于:

  • Skill 已经真实影响 Runtime
  • 前端仍然可以通过现有 reasoning / tool / text 协议感知这一变化

所以前端这一版主要延续的是:

  • useChatStream 消费结构化流
  • Tool card 显示工具调用过程
  • Skill 通过请求中的 options.skill 默认启用

关键代码:前端默认启用 utility-skill

这段代码的作用是:/instamind 默认工作在 utility-skill 模式下。

const DEFAULT_SKILL = 'utility-skill'

const requestBody: ChatRequest = {
  conversationId: conversationIdRef.current,
  messages: buildRequestMessages(nextMessages),
  options: {
    model: DEFAULT_MODEL,
    enableReasoning: true,
    skill: DEFAULT_SKILL,
  },
}

这一点让我能在不加新 UI 的情况下,先把 Skill Runtime 验证起来。

这版的真实回归,最说明问题的是什么

v0.0.7 我没有只看“代码能编译通过”,还单独做了一轮真实回归。

回归里比较关键的结论有这些:

稳定的部分

  • 普通开放式问答正常
  • calculator 正常
  • datetime(action=now) 正常
  • unit-convert 正常
  • text-transform 正常输入路径可用
  • 非法 JSON 会返回 tool-error

最说明版本边界的部分

  • utility-skill 已经能真实约束 Tool 使用范围
  • 多 Tool 仍然可以在 Skill 下保持统一输出风格
  • 但某些边界场景是否继续做更强兜底,已经不是这版的主问题,而是下一版是否继续打磨的问题

这个结论对我来说非常重要,因为它意味着:

v0.0.7 已经把 Skill Runtime 这条主链真正接通了,而版本主题也还保持清晰。

当前边界:这版已经做到了什么,还没做到什么

已经做到的

  • Skill Definition / Skill Registry 已落地
  • utility-skill 已能真实约束 Runtime
  • unit-convert 已接入
  • 版本材料和回归记录已同步

还没做的

  • 自动 Skill 路由
  • 多 Skill 串联
  • Skill 级记忆
  • Skill UI
  • MCP 接入
  • Agent 化执行

这些都不是遗漏,而是我在这版里刻意没做。

因为这篇文章真正想讲清楚的,不是“系统又多厉害了”,而是:

版本边界控制本身,就是 Runtime 架构能力的一部分。

这一版我最想留下的结论

如果要用一句话概括 v0.0.7,我会写:

这不是“多加一个 unit-convert”的版本,而是项目第一次正式把 Tool Runtime 往上抬了一层,长出了第一层 Skill Runtime。

再具体一点,这一版最值得记住的 4 个结论是:

  1. 多 Tool 之后,真正的难点不再是“能不能调 Tool”,而是“能不能稳定管理 Tool 边界”
  2. Skill 的价值不在于多一段 Prompt,而在于它是否真实约束了 Runtime
  3. 版本主题清晰,比把所有边界问题都塞进同一版更重要
  4. Tool、Skill、MCP、Agent 更像一条能力演进链,而不是并列功能清单

下一步会往哪走

如果继续往后推进,我觉得最自然的方向不会是立刻做 Agent,而是:

  • 继续收口 utility-skill
  • 评估下一版是继续做更多 Skill,还是进入 MCP 试点

如果说 Tool 是原子能力,Skill 是能力模式,那么下一步就会开始进入:

  • 外部能力接入标准
  • 更高层的任务模式
  • 更完整的 Agent Runtime

但那已经是后面的故事了。

最后

这个项目还会继续沿着 Skill Runtime、MCP、Agent Runtime 这些方向迭代下去。

如果这篇文章对你有帮助,欢迎到 GitHub 看看项目,也欢迎顺手点个 Star,这会是我继续更新下去的很大动力。

仓库地址: github.com/HWYD/ai-min…

❌