写这篇文章的起因是我在维护一个内部叫“agent-skills”的技能库项目。这个项目解决的问题很直接大模型智能体要真正落地光有推理能力不够必须挂载一批可靠、可复用、可观测的“技能”让模型不仅会聊天还能老老实实干完一件事。如果你正在做 Agent 应用或者被“模型乱调工具、技能一多就崩、扩展全靠改提示词”折磨过这篇应该能帮你省下不少弯路。所谓 agent-skills本质上是把智能体能执行的原子操作抽象成统一接口再通过注册、路由、编排把它们组装成复杂任务。我会从设计思路讲起再用完整代码带你把一个最小可用的技能框架跑起来最后重点聊聊生产环境里那些文档不会写的坑——比如参数解析、超时重试、技能调用的幂等性以及怎么给技能质量定一个可量化的评估标准。1. 项目概述agent-skills 到底是什么先澄清一个容易混淆的点开源社区里有个带情绪色彩的项目叫“Agent Skills”也有团队把函数调用或 tool use 直接等同于技能还有不少人把 MCPModel Context Protocol里的工具叫技能。在我这里agent-skills 的定义更窄也更务实它是一套围绕“模型能力边界”设计的可插拔技能系统包含技能定义、技能注册、技能调度、技能评估四层。1.1 为什么智能体需要“技能”而不是单纯堆 Prompt早期我做智能体喜欢把所有指令塞进 system prompt比如“你会调用搜索、会计算、会写SQL”。结果模型一多轮对话就开始乱明明该查数据库它偏要编一个查询结果该调用计算器它却自己心算。问题根源在于模型对“会”和“能”的认知是模糊的。技能体系解决的就是这件事把能力从提示词里拆出来变成显式的、可校验的代码模块。模型不再“假装会”而是必须走技能注册表匹配到具体函数再由函数返回结构化结果。这就像你招人与其在岗位描述里写“熟悉各种工具”不如明确列出“必须能操作 Excel 透视表”——技能就是那个明确清单。从工程视角看技能化还有一个巨大好处可以独立测试和灰度。原来改一个提示词可能要重新评估整体效果现在技能是独立模块单测过了再上线风险完全可控。1.2 agent-skills 要解决的核心问题我梳理了一下做技能体系本质上要回答四个问题技能怎么描述模型怎么知道某个技能“是干什么的、输入什么、输出什么”技能怎么发现几十上百个技能时怎么在不高昂的代价下匹配到最合适的那个技能怎么执行参数校验、错误处理、超时重试、并发控制这些工程化细节谁来管技能怎么迭代线上效果怎么评估技能质量怎么量化怎么防止退化这四个问题贯穿 agent-skills 整个项目。后面每一章都会对应解决其中一两个。你拿这套框架去接 OpenAI 的 function calling、接开源模型的 tool-use或者自建调度层思路完全一致只是适配层写法不同。2. 整体设计与技能分类搭框架前的关键决策很多项目死在第一步技能定义太随意。有人用纯文本描述技能有人用 JSON Schema 却忽略描述信息还有人把所有技能塞进一个巨大的类里面。我建议先定三个核心规范再动手写代码。2.1 技能接口的统一规范每个技能固定两段式结构描述信息 调用签名。描述信息里必须有名称、用途、适用场景、注意事项这部分是给模型看的调用签名则用 JSON Schema 描述参数结构这部分是给解析器用的。我在 agent-skills 里给每个技能设计了一个标准元信息模板字段含义重要性name技能唯一标识小写下划线模型依赖它做路由description说明用途、适当时机、边界条件决定模型能否正确匹配parametersJSON Schema 参数定义决定入参能否被正确解析returns返回值结构描述决定下游能否正确处理结果timeout超时时间避免技能卡死整个Agentidempotent是否幂等能否安全重试决定重试策略description 的价值经常被低估。模型不读你的 Python 注释只读这个字段。写描述时我习惯用“某某场景下当用户需要某某结果时使用此技能完成某某动作”的句式并且明确写出不适用的情况。比如搜索技能描述里加上“如果用户问的是天气请勿使用此技能”。2.2 技能分类从“只读型”到“写入型”按副作用强弱我把技能分成三类它们的工程要求完全不同只读查询型查数据库、查知识库、调第三方检索 API。特点是副作用小可以放心重试失败影响面小。计算/转换型算表达式、处理文本、转换格式。无副作用且确定性高适合做模型的可信计算器。状态写入型发邮件、写工单、改配置、下单。副作用不可逆必须要求用户确认或至少设计幂等键。分类的意义在于调度策略设计。只读技能可以并行执行和自动重试写入型技能必须加确认环节必要时要记录审计日志。这个设计做在后面第三章的调度器里但接口定义阶段就得把元信息留好。2.3 技能注册表与依赖管理我见过不少团队把技能写死在 Agent 的 if-else 里这样做的后果是技能一多代码就烂掉。agent-skills 的做法是做注册表模式技能通过装饰器注册到全局注册表Agent 启动时从注册表构建能力清单运行时基于模型打分路由。依赖管理方面技能之间尽量保持独立。如果确实要做交叉调用不要直接函数调用而是通过 Agent 编排层进行否则会打破“技能只做原子操作”的约束。我的经验是技能做小、做纯组合能力交给 Planner这样单个技能挂了不会拖垮整个链路。3. 实操从零实现一个最小可用的 agent-skills 框架下面这段代码是可以在本地直接跑的。为了演示清晰我会实现三部分注册器、技能示例、基础调度器。完整代码在 250 行以内适合做骨架后按团队需求扩展。3.1 注册器用装饰器把技能“挂”到注册表设计初衷很简单定义技能函数时顺手用装饰器把元信息写进去。# skill_registry.py from typing import Callable, Any, Dict import inspect import json class SkillRegistry: def __init__(self): self._skills: Dict[str, dict] {} def register(self, *, name: str, description: str, parameters: dict | None None, returns: str , timeout: int 30, idempotent: bool False): def decorator(func: Callable) - Callable: if name in self._skills: raise ValueError(fduplicated skill name: {name}) self._skills[name] { name: name, description: description, parameters: parameters or {type: object, properties: {}}, returns: returns, timeout: timeout, idempotent: idempotent, func: func, } return func return decorator def get(self, name: str) - dict | None: return self._skills.get(name) def list(self) - list[dict]: # 返回给模型的技能清单只含元信息不含函数实现 return [ {k: v for k, v in skill.items() if k ! func} for skill in self._skills.values() ] def call(self, name: str, arguments: dict, timeout: int | None None) - Any: skill self.get(name) if not skill: raise KeyError(fskill not found: {name}) func skill[func] t timeout or skill[timeout] # 简单参数校验与 JSON Schema 的 required 字段对齐 required (skill[parameters] or {}).get(required, []) for key in required: if key not in arguments: raise ValueError(fmissing required argument: {key}) sig inspect.signature(func) return func(**arguments)这段代码的核心点在于register装饰器把函数实例和元信息绑定在一起list方法输出的是纯元信息列表可以直接序列化后送给模型模型基于这个列表选择该调用哪个技能。3.2 实现两个典型技能计算器与知识检索下面演示一个“计算型”技能和一个“查询型”技能。第一个是确定性很高的计算器用 AST 而不是 eval 来保证安全# skills_impl.py import ast import operator from skill_registry import SkillRegistry registry SkillRegistry() safe_operators { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg, ast.Mod: operator.mod, } def safe_eval(expression: str): tree ast.parse(expression, modeeval) def _eval(node): if isinstance(node, ast.Expression): return _eval(node.body) if isinstance(node, ast.BinOp) and type(node.op) in safe_operators: return safe_operators[type(node.op)](_eval(node.left), _eval(node.right)) if isinstance(node, ast.UnaryOp) and type(node.op) in safe_operators: return safe_operators[type(node.op)](_eval(node.operand)) if isinstance(node, ast.Constant): if isinstance(node.value, (int, float)): return node.value raise ValueError(unsupported constant) raise ValueError(unsupported expression) return _eval(tree) registry.register( namecalculator, description计算数学表达式支持加减乘除、幂、取模。适合数值计算场景。, parameters{ type: object, properties: { expression: { type: string, description: 合法的数学表达式比如 (35)*2 } }, required: [expression], }, returns计算结果的字符串, timeout5, idempotentTrue, ) def calculator(expression: str) - str: try: result safe_eval(expression) return str(result) except (SyntaxError, ValueError, ZeroDivisionError) as e: return fERROR: {e}第二个技能模拟知识库检索实际项目里可以替换成向量检索或者 SQL 查询# 模拟知识库 fake_kb { agent: 一个能够感知环境并采取行动以实现目标的系统, skill: 智能体可调用的原子能力模块有清晰的输入输出定义, mcp: Model Context Protocol用于标准化工具接入的协议, } registry.register( nameknowledge_search, description在内部知识库中按关键词检索概念定义适合回答术语解释类问题, parameters{ type: object, properties: { keyword: {type: string, description: 要查询的关键词或短语} }, required: [keyword], }, returns匹配的知识条目列表, timeout3, idempotentTrue, ) def knowledge_search(keyword: str) - str: results [f{k}: {v} for k, v in fake_kb.items() if keyword.lower() in k.lower()] return \n.join(results) if results else NOT_FOUND这里我特意让calculator返回一个字符串因为 Agent 最终消费的永远是字符串——要么展示给用户要么作为工具结果再喂给模型。不要返回结构体对象然后期待模型能直接理解序列化成文本是最稳的传输方式。3.3 调度器技能路由与超时控制最小可用的调度器只做三件事把模型输出的结构化指令解析出来、从注册表找到技能、带超时地执行并返回结果。现在很多模型已经支持原生 function calling返回的就是结构化的 skill_name 和 arguments这种情况下解析逻辑会很轻。为了演示不依赖特定厂商 SDK我写一个通用的 dispatch 函数兼容“模型已经选好技能”和“需要模型自选技能”两种情况# dispatcher.py from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout from skill_registry import SkillRegistry from skills_impl import registry class SkillDispatcher: def __init__(self, registry: SkillRegistry): self.registry registry self._executor ThreadPoolExecutor(max_workers4) def dispatch(self, skill_name: str, arguments: dict, timeout: int | None None) - dict: skill self.registry.get(skill_name) if not skill: return {ok: False, error: funknown_skill: {skill_name}} future self._executor.submit(self.registry.call, skill_name, arguments) try: result future.result(timeouttimeout or skill[timeout]) return {ok: True, result: result, skill: skill_name} except FutureTimeout: future.cancel() return {ok: False, error: ftimeout after {timeout or skill[timeout]}s} except Exception as e: return {ok: False, error: str(e), skill: skill_name} def available_skills(self) - list[dict]: return self.registry.list()注意我把超时控制放在线程池的future.result(timeout...)层而不是函数内部。这样即便技能函数本身有死循环或者外部 API 卡住Agent 也不至于被拖死。实际生产里还可以把阻塞型技能改成异步协程但线程池方案胜在改动小、兼容老代码。4. 技能编排与多技能协同从单个技能到完整任务框架跑通后真正的挑战是编排。模型用户不会只要求“计算一下 35他们会问“查询一下 transformer 的定义然后对比它和 RNN 的差异再总结成一段话”。这种任务至少涉及检索两个概念、可能再触发一次计算或者生成对比表。靠单个技能无法覆盖需要编排。4.1 意图路由怎么选中最合适的技能如果用的是带 function calling 的模型路由模型内部已经做了如果是自建路由最朴素的方案是基于技能描述和用户意图的相似度打分。我做过一个不依赖外部向量库的版本用字符级 TF 特征就能跑出可接受的效果核心逻辑是把用户输入和每个技能的 description 做关键词重叠度计算def route(intent: str, skills: list[dict]) - str: intent_tokens set(intent.lower().replace(?, ).split()) best_name None best_score 0.0 for s in skills: desc_tokens set(s[description].lower().split()) overlap len(intent_tokens desc_tokens) score overlap / max(len(intent_tokens), 1) if score best_score: best_score score best_name s[name] return best_name, best_score这版“词袋匹配”只适合做教学演示生产环境建议用 embedding 相似度。但不管怎么做有个原则必须守住路由结果要有分数低于阈值要能说“不知道”而不是硬选一个。宁可交给兜底逻辑或者让模型生成澄清问题也不要让 Agent 假装修理好了。4.2 两种组合模式顺序执行与分解执行我发现团队里最常见的编排诉求是“先查后算再写”。按数据流方向可以抽象出两种模式顺序流水线模式技能 A 的输出作为技能 B 的输入。典型场景是“查表拿指标再算同比再画趋势”。实现上需要约定技能之间的数据契约也就是前面提到过的 returns 字段。为了兼容模型消费我习惯让技能返回纯文本然后在下游技能的参数构造时用正则或者再调用一次模型把文本填入参数。任务分解模式把一个复杂问题拆成多个子问题每个子问题走一次“路由-执行”。典型场景是“对比 transformer 和 RNN”。Planner 先把任务拆成“查 transformer”“查 RNN”“生成对比表”三步然后循环调度。核心技巧是维护一个步骤间的上下文缓冲区让后续技能能看到前面步骤的结果摘要否则模型会丢失信息。4.3 上下文管理与记忆技能的取舍技能切得越细上下文管理越难。每次工具调用的结果都会重新喂给模型Token 消耗很快。我的经验是三步策略摘要再入、过滤噪声、过期淘汰。摘要再入不是把完整工具结果塞进历史而是先生成一段 70 字以内的摘要。这一步可以用一个小模型甚至规则抽取首句来完成成本低、效果好。过滤噪声技能返回里往往有大量日志或调试信息我在技能实现层面约定“returns 只该有用户关心的结果”把日志打到 stdout 而不是塞进返回。过期淘汰多轮任务里前几步的历史可能已经无价值只保留与当前步骤相关的窗口。记忆技能则单独封装比如“写入短期记忆”“读取历史关键信息”。这个技能和其他技能同等地位只是它的存储介质是内存或 Redis好处是 Agent 能显式地保存中间状态而不是依赖超长上下文。5. 生产环境中的踩坑与优化技能系统的“野生指南”上面 250 行代码只是能跑。真正上线会被各种边角问题打爆。我把这一年多生产环境里踩过的坑集中整理一下希望你不用重走。5.1 参数解析模型总是不按 Schema 传参怎么办即便给了 JSON Schema模型仍然可能传错类型。最常见的是把数字参数传成字符串或者少传必填字段。我现在的做法是三层兜底第一层在 SkillRegistry.call 里做 required 字段检查缺参就报错绝不让技能函数内部去猜。第二层对基础类型做宽松转换。字符串类型的数字、带单位的值统一转成标准类型。比如 3.5秒 提取出 3.5。第三层对于枚举类参数传入非法值时自动映射到最接近的合法值并在结果里标注修正行为。这套兜底大幅提升了调用成功率代价是需要维护一个轻量转换器但它比每次失败后重新调用模型补参便宜得多。5.2 超时、重试与幂等性设计老生常谈但值得再说技能一旦挂起整个 Agent 对话就卡住了。我建议所有技能默认超时 10 秒重试只在技能标记为 idempotent 时才启用。非幂等技能被重试会导致重复发邮件、重复下单这类事故。幂等设计有个简单方案给每个操作技能增加一个request_id入参执行前先查“这个 request_id 是否处理过”处理过就直接返回上次结果。这个 request_id 由 Agent 生成同一意图的多次重试复用同一个 id。成本不高收益量级是“从灾难级降到可恢复”。5.3 评估技能质量怎么量化技能多了以后人的判断不再可靠。我给 agent-skills 搭了一套轻量评估管线针对每个技能准备 10 到 20 条黄金样本每条样本包含输入、期望输出、期望行为成功/失败/需要澄清。然后跑三个指标技能选择准确率Agent 是否选对了技能。参数构建准确率参数是否合法、是否包含必要信息。执行成功率技能本身是否成功处理。这三个指标互相独立能快速定位退化发生在哪一层。每次改技能描述、改路由阈值或者换底层模型时都跑一遍数字变化比感觉可靠得多。5.4 常见问题速查表现象可能原因解决思路模型反复调用同一个技能但不推进技能返回值无效模型缺少停止条件检查 returns 语义增加“无结果请停止”的指令技能匹配经常选错技能 description 写得太笼统彼此区分度低重写 description明确边界和不适用场景参数校验频繁失败模型不熟悉参数格式或样例太少减少必填字段增加默认值提供 few-shot 示例技能执行时间过长外部 API 慢或代码低效分层超时外部调用单独设置短超时重试退避技能结果互相矛盾多个技能引用同一数据源但缓存策略不同统一数据源和缓存键技能间共享快照还有一个隐藏很深的坑技能函数里千万不要直接打印敏感参数。日志会随着 Agent 链路进入审计系统一旦技能处理的是用户隐私数据日志泄露就是合规事故。我在框架里对所有技能函数做了统一包装默认只记录技能名、出参摘要和耗时不记录入参原始值需要明细时再单独开白名单。最后分享一点体会从最初十几个技能到现在上百个技能我最大的感受是技能体系真正的门槛不在写函数而在定义边界。一个技能的 description 写得好不好直接决定模型能不能正确调用它一个技能有没有设计幂等键直接决定故障恢复时你敢不敢重试一套技能有没有评估管线直接决定你迭代的时候是“越改越好”还是“越改越乱”。我建议你上手时不要贪多先选 3 个高频、低风险的技能跑通整条链路等注册、路由、调度的基建稳定了再逐步扩充。技能库这个东西前 20 个是红利期后 80 个是管理期管理的复杂度不会因为你模型换得更强就消失反而会随着 Agent 使用频率上升被放大。把 agent-skills 当成一个需要持续运营的系统来对待而不是一次性写完的工具集这才是它真正值钱的地方。
企业数字化 ERP 产品动态
相关推荐
二阶巴特沃斯带通滤波器:MATLAB实现与工程避坑指南 1. 从哪里开始:为什么一个“二阶带通”值得专门写一篇先交代下背景。我最近在调一个振动信号分析的小项目,传感器采回来的数据里,既有设备本身的工频干扰,又有我们真正关心的9~11Hz特征分量。目标很明确:把有用频段提出… · 2026/9/25 16:13:21
VMware Workstation安装CentOS 7.9实战指南 1. 项目概述:为什么现在还要手把手教VMware装Linux?“VMware虚拟机安装Linux教程(超详细)”——这个标题看起来像十年前的老古董,但现实是:我上周刚帮三位刚转行的运维新人重装了第5台CentOS 7.9虚拟机&… · 2026/9/25 16:13:15
bb 插件开发入门:如何用 Plugin SDK 从零构建第一个可用插件(8 步完整教程) bb 插件开发入门:如何用 Plugin SDK 从零构建第一个可用插件(8 步完整教程) 【免费下载链接】bb The agent IDE that builds itself 项目地址: https://gitcode.com/gh_mirrors/bb14/bb
bb 是一款"The agent IDE that builds its… · 2026/9/25 16:42:24
ax:面向Agentic工作负载的Kubernetes调度编排CLI实践 1. 从“ax”这个标题说起:一个被低估的Agentic调度入口第一次看到“ax”这个标题,很多人会以为是某个命令行工具的缩写,或者某个内部项目的代号。但把热搜词摊开来看——ax、agentic、orchestrator、Kubernetes、CLI——这几个词凑在一起&… · 2026/9/25 16:42:18
Atlas 300V 24G算不算运算加速卡?YOLOv8部署实战解析 前两天一个做边缘视觉的朋友发来消息,开口就问:Atlas 300V 24G到底算不算运算加速卡?紧接着又追了一句——我现在想在这个卡上把YOLOv8跑起来,方便吗?这个问题其实非常典型。很多搞算法的人第一次见到昇腾Atlas系列硬件… · 2026/9/25 16:42:12
Langchain 从部署到调用各大主流厂商大模型使用详解 目录 一、前言
二、Langchain 介绍
2.1 Langchain 是什么
2.2 为什么要学习 LangChai
2.3 LangChain 的核心组件
2.4 LangChain 分层架构图
2.5 LangChain 使用场景
三、Langchain 安装与使用
3.1 环境准备
3.2 Langchain 安装过程
3.2.1 一键安装依赖包
3.3 对接三… · 2026/9/25 16:41:47
Jev:AI 圈的“决策模型”,以及它在国际银行贷款场景的落地方案 一、Jev 到底是什么Jev 是 TypeSafe AI(前 OpenAI 研究员 Diogo Almeida 创立)在 2026-09-15 发布的第一个 System One Model(系统一模型)。它不聊天、不写代码、不写报告,只做一件事:非结构化状态进 →… · 2026/9/25 16:41:47
React性能优化没效果?你可能漏了这个小技巧 "明明用了React.memo、useMemo,列表项也加了key,为什么页面的渲染性能还是没提升?"——去年在做一个大表单编辑器时,我对着性能面板上超过200ms的渲染耗时抓耳挠腮,直到发现一个被多数优化指南忽略的隐形杀手… · 2026/9/25 16:41:35
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37