Claude Code 的 Skill 系统的业务化改造
skillbay:把 Claude Code 的 Skill 系统改造成后端服务能用的样子
更新于 2026.08.31
本文目录 7 节

skillbay:把 Claude Code 的 Skill 系统改造成后端服务能用的样子
https://github.com/Ezzzi-Y/skillbay
LLM 应用做久了都会撞上同一个问题:一个 Agent 要掌握很多能力。客服要懂退款政策、物流规则、发票开具;HR 助手要懂请假、报销、招聘流程;运维 Copilot 要能查公司内部系统。但模型一次能带进上下文的东西是有限的——把几十份操作手册全文塞进 system prompt,第一轮就把上下文烧光了,后面什么都干不了。Claude Code 的 Skill 系统是对这个问题很干净的一个回答:一份能力就是一个目录,目录里是 SKILL.md 加参考资料。模型平时只看到一份很小的技能清单(预算约 1% 上下文),真正需要时才加载对应技能的全文。这套机制解决了”一个 Agent 携带多种专业能力”的问题,而且把成本控制在可预期范围内。
但这套机制有一个前提:它是为”开发者在自己终端里干活”设计的。把它原样搬进后端业务服务,有几处规则必须改。这就是 skillbay 这个项目在做的事——语义上 1:1 移植到 LangChain 的 middleware API 上,然后在”场景使然”的地方做刻意的偏离。下面说说这些偏离。
先看场景差异
Skill机制是通用的,但是服务场景是不同的。 对于Claude Code这类提供给开发者的Coding Agent,这类应用应当给使用者提供高度可定制的能力;但是业务服务完全与这相反,业务服务需要的是安全、稳定、可审计,因为Agent操作的是公司账户上实打实的钱。
改造一:技能是部署产物,不是运行时发现
Claude Code 里,安装技能的人就是使用技能的人,运行时发现 Skill 是合理的——出问题最多坑自己。业务服务打破了这个对称:开发者提供 Skill,使用的人是客户,承担后果的人是公司。让一个技能在挂载目录里”冒出来”,等于把未经评审的行为直接推到顾客面前:没有 review、没有版本、没有回滚。
所以 skillbay 在构造时一次性加载技能集合并锁定:
- 技能进 git、走 code review、随版本发布;
- 运行期的文件变更在进程重启前不生效;
- 技能身份以目录名为准,frontmatter 里的
name只是显示名——磁盘上的改名不会悄悄改变路由行为。
改造二:allowed-tools 工具闸
业务服务里的 Agent 以公司的身份行事,客服技能绝不能触及自身职权之外的工具。技能的 frontmatter 可以声明 allowed-tools;该技能生效期间,中间件的 wrap_tool_call 钩子会对每一次工具调用强制执行白名单。不是”相信模型会遵守”,而是让它根本执行不了。
生效窗口完全从消息历史推导,没有额外状态可以污染:
- 技能调用成功时打开——其 ToolMessage 以
Launching skill:开头; - 下一条真实 user 消息到达时关闭,
<system-reminder>注入不算新任务; - 多个技能同时生效时白名单取并集,且只紧不松——没声明
allowed-tools的技能永远不能放宽已生效的限制; - 窗口内连
skill工具本身也拦,封死”先调受限技能、再链一个不带限制的技能”的提权路径。
另外,被权限策略拒绝的技能调用不会产生 Launching skill: 标记,所以被拒的技能永远不会激活它的白名单——拒绝不能成为提权通道。技能正文因摘要压缩而重注入时,也会重新过一遍权限策略。
改造三:记账放进 agent state,而不是进程全局字典
Claude Code实现把已播报/已调用的技能记在模块级 dict 里。一个开发者的单会话进程无所谓;客服系统要同时服务成千上万路可恢复、带 checkpoint 的会话,这么做就是错的。skillbay 把两本账(announced_skills / skill_invocations)都放进 SkillState,它是 AgentState 的扩展,换来三件事:
- 持久化——记账随 checkpoint 存活,进程恢复后不会重复播报;
- 隔离——按 thread 隔离,并发会话互不串扰;
- 压缩存活——当摘要中间件把调用技能的那一轮对话压缩掉时,调用记录仍在 state 里。
before_model发现tool_call_id从消息列表里消失,就重新展开正文并注入。技能的指引活过了杀死它的转写记录的那次压缩。
最后一条是这类系统最容易翻车的地方:技能正文一旦被摘要压缩掉,模型就”失忆”了,但会话还没结束。记账放 state 里,压缩只能删消息,删不掉状态。
改造四:技能退场——模型可以释放不再需要的技能
其他技能系统(Claude Code、Codex、Cursor,以及我们见过的每一个 LangChain Skill 中间件)都把技能激活当作”一发不可收回”:一旦正文被注入,它就一直占用上下文,直到对话结束或上下文被压缩掉。这对开发者会话没问题——人类知道任务何时结束。但服务代理处理的是多轮对话,话题会自然漂移:客户先问退款政策,然后转向问物流时效——退款技能的 12 步流程继续占着上下文,没有任何意义。
skillbay 引入了技能退场:模型可以调用 skill_dismiss 释放不再匹配当前对话范围的技能。退场后,该技能的全文不再被重注入,退场状态持久化在 agent state 里,同时产生一条 skill_dismissed 审计事件记录原因。这是一个小机制,但对长期服务对话很重要:一是减少后端服务消耗的 Token 数量,二是尽力避免话题漂移。
改造五:鲁棒性设计
一个技能写坏,拖不垮启动。 frontmatter 解析永不抛异常:先试原文,失败后自动补引号重试(防住 paths: **/*.{ts,tsx} 这类经典手误),再失败降级为空头。缺 description 的技能跳过并告警,而不是致命错误。
mw = SkillMiddleware(
skills_dirs=["skills"],
audit=lambda event: print(event), # 接到你的可观测性栈
)
agent = create_agent(model, tools=[...], middleware=[mw])
审计是安全节点,不是装饰。 六类审计事件(skill_invoked、skill_denied、skill_reinjected、skills_announced、tool_call_blocked、skill_dismissed)经由同一个回调缝发出,接到日志/指标栈即可;审计回调自己抛异常也不会拖垮 Agent。
评论
无需登录,审核通过后公开。只有博主可以回复。
正在加载…
已公开的评论