
我过去一年一直在做智能体相关的工作最大的感受是大模型本身越来越聪明但真正把它用到业务里卡住的往往不是模型智商而是“够不着真实世界”。你让它查个库存、发个通知、跑个脚本它做不到因为它只会生成文本。agent-skills 这套东西就是冲着这个问题去的——把模型和外部执行环境之间缺的那一层“技能层”补齐。本文想把我整理这套技能系统的思路、接口设计、最小实现和踩坑记录都摊开来讲给正在做智能体应用的开发者一个可参考的底稿。1. Agent-Skills 是什么把“会说话”变成“会干活”1.1 先从一个让人头疼的场景说起我接手过一个内部智能助理项目最开始的需求很简单员工在对话框里提问模型给出答复。上线一周后真实的用户需求开始露出来比如“帮我查一下这个月的发货数据”“把这份合同摘要发我”“给项目群发一条明天开会的提醒”。这些事模型一件都做不了。它没有数据库连接没有文档库权限也没有 IM 的发送接口。你逼它回答它就给你编发货数据是假的合同摘要是凭空总结的提醒是永远发不出去的。这也是很多智能体项目推进到一半突然卡住的原因。模型的理解能力已经够用但执行能力是零。agent-skills 的核心思路很简单不指望模型自己长出执行能力而是把能力做成一个个可插拔的技能模块由模型负责决策和生成参数技能负责真正干活。模型说“我想查发货数据”技能就去连数据库把数据取回来再交回给模型组织成回答。模型从“答案的生成者”变成了“任务的调度者”工具从“模型手上没有”变成了“挂在技能层随取随用”。1.2 技能、工具与插件的边界做这个项目之前我一直把“工具”“插件”“技能”当成同一件事后来发现不把它们掰开后面做设计会越来越乱。工具是最底层的执行单元一个函数、一个 API 调用、一段脚本都算工具它解决的是“有没有能力执行”的问题。插件是工具的常见打包形态很多宿主应用用插件机制来扩展能力本质还是把工具挂上去。技能则是在工具之上叠加了一层完整的使用规范包括什么场景调用它、参数怎么填、前置条件是什么、结果如何校验、如果失败怎么降级。你可以有一个“发邮件”的工具但一个完整的“邮件发送技能”还必须包含收件人格式校验、正文安全审查、发送重试策略、失败时的错误码定义。用厨房类比最直观。工具是菜刀、砧板、炉灶插件是一套“西式厨具包”而技能是一名知道什么时候该切、什么时候该剁、切完怎么清理、灶台温度怎么控的厨师。工具解决“有没有”技能解决“会不会用、该不该用、怎么安全地用”。这个区别在智能体里尤其重要因为大模型是概率输出你不给工具加规则它就可能拿菜刀去削水果、拿削皮刀去剁骨头。技能层本质上是对工具使用过程的一次工程化约束。1.3 一套技能系统的四个核心模块我在 agent-skills 里把系统分成了四块技能注册中心、技能调度器、技能执行器、反馈评估模块。一开始没必要做得重但模块边界要清楚后面扩展技能的时候会少很多返工。技能注册中心是“技能清单”维护每个技能的名称、描述、参数 Schema、超时时间、风险等级、调用入口。所有技能上线前先在这里登记对外暴露的唯一接口就是查清单。技能调度器负责“决定调用谁”它接收大模型传过来的意图和参数做格式校验、参数映射、优先级判断决定是串行调用、并行调用还是直接拒绝。技能执行器负责“真正干活”运行在受控环境里做超时熔断、重试审计、资源隔离它不关心模型怎么想只关心任务能不能在期限内完成。反馈评估模块负责把结果转成模型能读的观察摘要同时记录成功率、耗时、错误分布供后续迭代使用。这四个模块的协作关系可以参考餐厅后厨注册中心是菜单调度器是传菜员执行器是后厨团队反馈评估模块是试菜员。客人点什么传菜员下单后厨出菜试菜员确认口味没问题再端出去。任何一环掉链子出来的都不是一道能上桌的菜。2. Agent 技能的设计原则接口、分类与边界2.1 接口标准化参数的声明、校验与兜底技能系统第一个要定的规矩就是接口。我在项目里被模型坑过太多次参数类型传错、必填字段漏传、枚举值写了个模棱两可的表述结果执行器拿到参数后直接崩掉或者更糟——参数虽然类型对但含义完全不是用户想要的。后来我学乖了所有技能在上线前都要有显式的参数声明我一律用 JSON Schema 风格的描述既有可读性又能自动生成校验逻辑。一个典型的技能定义长这样name: web_search description: 搜索互联网并返回相关网页的标题和摘要用于获取实时信息 parameters: query: type: string required: true description: 搜索关键词尽量简短明确 max_results: type: integer required: false default: 5 minimum: 1 maximum: 10 timeout: 15 risk_level: read retry: 1每个技能提前把参数的类型、是否必填、默认值、取值范围写死。模型生成调用参数时调度器先做一次严格校验不通过就直接返回参数错误绝不进入执行器。这套流程看起来多了一步实际上省了无数排查时间。没有校验你分不清某次失败是模型抽风还是执行器 bug有了校验失败原因马上能落到具体字段。接口层还有一个容易忽略的点技能的超时和重试策略必须在定义里写明。我用了一个简单的约定执行器在技能设置的时间内拿不到结果立即返回超时错误码超时后如果技能声明允许重试最多重试一次且重试前必须确认幂等。像“发送邮件”这种重发了会产生重复邮件的技能我宁可让它直接失败也绝不重试。2.2 风险分级技能不是越多越好而是越可控越好技能做到后面你会发现难点不是“做不出来”而是“做出来之后敢不敢放开用”。我给技能分了四级风险不同风险等级对应不同的审批和执行约束。这个分类标准我用下来很有效新技能上线时先定级再决定要不要接进主链路。低风险技能是只读类操作比如网页搜索、文档解析、信息查询它们不产生外部副作用可以放给模型自由调用。中风险技能会产生轻度副作用比如发送通知、创建工单、修改某个临时配置这类技能要求模型调用前向用户展示参数并经过确认。高风险技能会影响真实数据比如删除记录、修改生产配置、触发批量任务要求二次确认加操作审计而且执行环境里要限制目标范围。最高风险档是资金支付类操作这一档我基本不交给模型自主决策走人工审批流程模型只能帮忙填单子。分级的意义是什么不是限制模型能力而是让风险可控。智能体一旦接入真实业务每次模型调用后面的安全成本是不一样的。你给所有技能一样的权限等于给所有操作都开了最高风险的口子你给所有技能一样的限制等于把所有能力都锁死。分级管理是我能找到的平衡点高频的只读技能跑得飞起敏感操作一步一停系统整体才敢放开。2.3 权限边界给技能套上沙箱与审计权限这件事我在 agent-skills 里把它拆成三个层面资源配置隔离、运行时隔离、行为审计。资源配置隔离说的是每个技能只给“够用”的凭证。比如数据库查询技能拿到的连接账号是只读账号更新操作根本不具备权限文件操作技能只能访问指定目录不能碰系统路径。这里的原则是最小权限——一个技能只能拿到它完成任务所必要的最小资源集拿不到任何多余的东西。运行时隔离解决的是恶意或失控执行。对于执行代码类的技能我会放在隔离容器里跑限制 CPU、内存、网络访问范围。技能内部产生的临时文件只存在于容器里结束后自动销毁。这里要提醒一点隔离不等于放任容器里跑的东西一样要做执行日志不能在沙箱里变成黑盒。行为审计则是在所有技能调用的入口和出口各记一条带流水号的操作记录记录谁在什么时间调用了哪个技能、传了什么参数、返回了什么结果。这套记录的价值是在事故发生后能追溯。有一次线上出现异常数据变更我靠审计日志半小时内锁定了是某个技能在特定参数下触发了不该有的写入然后立刻降级处理。如果当时没有审计这种问题排查起来会是一场灾难。3. 实操从零搭建一个最小可用的技能系统3.1 项目骨架技能注册与执行这一段全是可复用的代码我用 Python asyncio 实现了一个极简版本重点不在代码量而在把“注册—调度—执行”的骨架立起来。整个项目结构很小agent_skills/ registry.py # 技能注册中心 dispatcher.py # 调度器参数校验 选择执行 executor.py # 执行器真正跑技能逻辑 skills/ web_search.py sql_query.py report.py先定义一个技能的基础数据结构from dataclasses import dataclass from typing import Callable, Any dataclass class Skill: name: str description: str parameters: dict # JSON Schema 风格参数声明 executor: Callable # 实际执行函数 timeout: int 20 risk_level: str read retry: int 1注册中心就是一个全局字典和注册函数_skills: dict[str, Skill] {} def register_skill(skill: Skill) - None: if skill.name in _skills: raise ValueError(f技能 {skill.name} 已存在) _skills[skill.name] skill我特别加了“已存在则报错”的约束。技能重名的坑我踩过一次两个技能模块各自注册了同名入口后注册的覆盖了前注册的结果线上调用全走了错的那个。强制唯一性可以在项目初期就拦住这类低级失误。调度器接收模型传来的技能名和参数先查注册表再校验参数然后调用执行器async def run_skill(name: str, params: dict, context: dict): skill _skills.get(name) if not skill: return {code: 1001, message: f未知技能: {name}} # 参数校验 errors validate_params(skill.parameters, params) if errors: return {code: 1002, message: f参数错误: {errors}} try: result await asyncio.wait_for( skill.executor(**params, contextcontext), timeoutskill.timeout ) return {code: 0, data: result} except asyncio.TimeoutError: return {code: 1003, message: f技能执行超时{skill.timeout}s} except Exception as exc: return {code: 1004, message: f执行异常: {exc}}注意这里所有异常都被兜住统一转成结构化错误码返回给模型。智能体链路里模型的下一步决策依赖上一步返回的结果如果执行器抛出一个未捕获的异常整个会话就断了但如果你给它一个带错误码的返回模型反而可以根据错误信息调整重试或改问用户。这个设计是从无数次断线里总结出来的千万别图省事省略掉。3.2 三个基础技能搜索、SQL 查询与组合任务有骨架之后技能本身就好写了。三个有代表性的技能分别是只读搜索、受控 SQL 查询、组合任务。搜索技能发一个 HTTP 请求解析结果并截断async def web_search(query: str, max_results: int 5, context: dict None): # 以请求某个外部搜索服务为例 response await http_client.get( https://search.example.com, params{q: query, limit: max_results} ) items response.json()[items] return [ {title: item[title], snippet: item[snippet][:200]} for item in items[:max_results] ]这里两个细节值得说明。第一返回结果一定要截断。搜索摘要动辄几万字全量塞回给模型不仅浪费 token还会稀释焦点让模型在无关内容里迷失。第二返回结构要统一成列表每条只保留必要字段方便反馈评估模块整理成观察摘要。SQL 查询技能要加一层安全校验async def sql_query(sql: str, database: str, context: dict None): # 只允许单条 SELECT 语句 normalized sql.strip().rstrip(;) if not normalized.upper().startswith(SELECT): return {error: 仅支持 SELECT 查询} if ; in normalized: return {error: 不允许多语句执行} conn get_readonly_connection(database) try: rows conn.execute(normalized).fetchall() return {rows: rows, truncated: len(rows) 100} finally: conn.close()读权限账号、单语句限制、只允许 SELECT这三条是我给 SQL 技能定的红线。大模型天然会把用户口语里的“删掉”“改一下”也生成进 SQL 里你用权限和字符串拦截双重兜底才能保证它不是数据库里的定时炸弹。组合任务技能展示的是“技能如何编排其他技能”。比如让模型生成一份带数据的摘要它内部先调用 SQL 查询技能拿数据再调用文本摘要技能处理结果async def summarize_report(database: str, topic: str, context: dict None): rows await run_skill(sql_query, {sql: fSELECT ..., database: database}, context) if rows[code] ! 0: return rows summary await run_skill(text_summary, {content: rows[data]}, context) return summary组合技能是智能体真正发挥价值的地方。单技能解决的是单点问题组合技能解决的是“查完再算、算完再总结”的多步流程。但组合技能也意味着调度器要从“一次调用”变成“一次会话”模型需要在这个过程里不断拿到中间结果并决定下一步这就引出了对接方式的问题。3.3 大模型和技能的三种对接方式把技能系统建好之后还需要决定大模型怎么调用技能。我在实际项目中用过三种主流方式各自适配不同场景。Function Calling 是当前最常用的方式。大模型在生成回复时如果判断需要技能会输出一个结构化的函数调用请求包含函数名和参数对象宿主应用解析后执行并返回结果。它的优点是稳定、易解析参数天然是 JSON不需要从文本里抽缺点是模型通常只能输出单步调用不适合做很长的多步推理。ReAct 方式是让模型以“思考—行动—观察”的循环来推进。模型先输出思考过程再输出要执行的行动系统执行后把观察结果喂回去循环直到得出最终答案。它的优点是流程图长、能应对开放任务你可以看到模型一步步在推理缺点是容易陷入重复循环模型可能在同一个错误行动上反复打转必须设计全局重试预算。Plan-and-Execute 是先把任务拆成分步计划再执行。模型先生成一个计划列表每步对应一个技能然后按计划依次执行。它的优点是可控性强中途任何一步失败都可以在计划层面调整或回滚缺点是一开始计划生成得不准后面全盘崩掉。三种方式我做了个对比对接方式核心原理优点缺点适用场景Function Calling模型输出结构化调用参数稳定、易解析、单步可靠难以完成长链路多步推理单技能或短链任务ReAct思考-行动-观察循环灵活、适合开放型任务可能陷入重复循环成本不可控多步推理、探索型任务Plan-and-Execute先生成计划再分步执行可控制、可回滚、工程友好计划不准则全链路受影响流程固定的业务场景我的建议是不要把三种方式看成互斥选项而应该把它们组合起来。简单技能直接用 Function Calling复杂任务先走 Plan-and-Execute 生成计划计划内每一步再用 Function Calling 执行途中遇到意外结果时让模型启动 ReAct 来做局部调整。这套混合策略我在项目里跑得很顺比只押注任何一种方式都稳。4. 技能系统上线前后的高频踩坑记录4.1 五个最容易翻车的环节技能系统踩坑的规律性很强翻来覆去就是那几个环节。我把最常见的五个问题整理成了一个速查表遇到同类症状可以快速对号入座。问题现象根因应对手段幻觉参数模型编造不存在的数据库名、文档路径、工单号实体参数不在上下文中模型只能“猜”参数校验 快速失败 让模型向用户追问重试风暴同一技能反复失败反复重试成本指数上升没有全局重试预算失败时模型不断自愈但越试越糟设置单会话总重试次数和总耗时上限提示注入技能返回的外部内容里夹带“忽略之前指令”等恶意文本模型被带偏外部数据被当成可信指令处理外部返回内容做隔离处理不作为系统提示技能冲突两个并发技能修改同一资源数据互相覆盖缺少资源锁和冲突检测对写操作技能加锁必要时串行化执行回归漂移技能没变但升级模型后表现大幅变化模型版本升级导致行为不可预测准备技能评测集回归测试跑完整链路幻觉参数这个问题最隐蔽。之前有个客服场景模型需要根据用户提供的订单号查数据它居然编了一个格式很像、实际不存在的订单号过去查询技能正常返回空结果模型还在那煞有介事地跟用户说“您的订单不存在”。后来我在技能定义里增加了参数可验证性标记如果参数值无法从对话上下文中确认要求模型先向用户确认不许直接猜。这招治好了相当一部分“一本正经胡说八道”。重试风暴则是成本杀手。有一次跑一个批量数据分析任务模型连续调用同一个失败技能每次失败后换个参数重试前后调了四十多次费用差点失控。痛定思痛之后我在调度器里加了会话级重试预算全局最多重试 N 次超了就冻结技能调用让模型转向用户或走人工兜底。4.2 故障排查先定位再修复技能系统一旦上了生产排查问题的难度会直线上升因为链路变长了用户输入 - 模型理解 - 参数生成 - 调度器校验 - 执行器运行 - 结果回填 - 模型组织答案。任何一环出错现象都可能是“回答不对”。没排查手段的时候我只能靠肉眼推逻辑效率极低。后来我做了两件事排查效率翻了一倍。第一全局链路 ID。每次用户会话生成一个唯一 ID贯穿所有技能调用日志、模型输入输出记录、执行器运行记录。查问题的时候拿着一个 ID 就能把整条链路拉出来不用再靠时间戳拼图。第二结构化日志。每个技能执行的入参、出参、耗时、错误码全部按统一格式落盘。这一步很枯燥但它把所有“看起来像是玄学”的问题变成了可检索的数据。我还保留了一套回放工具把线上某次出问题的用户输入和当时链路数据存下来离线重放一遍观察模型和技能系统在相同输入下的表现。回放看起来费事但能够区分问题是偶发的模型波动还是稳定的系统缺陷。如果是稳定缺陷回放十次会复现十次定位和修复都有明确依据。4.3 让技能系统越用越稳的几条优化手段上线稳定之后我开始关注成本和效率。三条优化手段在实践中收益最大。第一个是技能路由缓存。很多技能调用是高频重复的比如查某个热门商品的库存、问某个公共政策的条文。我在调度器前加了一层带时效的缓存同一技能同一参数组合在短时间窗口内命中就直接返回缓存结果不再跑到执行器。上线后整体调用量降了不少执行器负载也轻了。第二个是并行执行。组合技能里如果有相互独立的环节我用 asyncio.gather 把它们并发跑起来。比如生成一份包含订单数据和物流数据的报表两个查询互不依赖串行要 6 秒并行 3 秒出头。这个优化不涉及模型能力纯粹是工程侧省时间但用户体感提升非常明显。第三个是降级链。每个关键技能我都设了一条备用路径。SQL 查询挂了走只读接口拉 CSV搜索服务超时走本地缓存的常用数据。降级链不是给人看的摆设而是保障智能体在依赖方故障时不至于全线瘫痪。注意降级路径同样要记录日志否则你以为技能正常其实已经在用二手数据了风险不小。5. 最后分享几点我自己的体会做完整套 agent-skills 之后我最大的体会是不要把“技能数量”当 KPI。技能越多注册中心越臃肿调度器的决策成本越高模型选错技能的概率也越大。一个几十个技能的规模可能看着很丰富但当模型面对一堆描述模糊的技能时它并不知道该调哪一个。我后期反而砍掉了一批低频技能把常用的十几个技能描述写得极其精确整体准确率明显上升。给模型看的那段技能描述要短、要具体、要有区分度。描述里不要写“这是一个非常好用的工具”要写清楚“什么时候用、什么情况别用、需要什么参数”。模型每轮决策都会把这些描述读一遍描述越长注意力越容易被稀释。我自己的标准是一行场景一行参数一行禁用前提超过三行就精简。另外技能系统上线之前一定要先准备一套带标注的评测集。不用很大几十条覆盖典型场景和边界场景的输入输出就够。每次升级模型、改技能定义、调调度策略先跑一遍评测集看分数有没有掉。没有评测集的技能系统就是在黑暗里裸奔出了问题都不知道是哪次改动引起的。最后技能系统天然是持续迭代的产物。第一版能覆盖 80% 常见场景就已经是成功的开始剩下 20% 靠日志、评测和反馈逐轮磨。如果你正准备建设自己的智能体技能层希望这套从设计、实现到排查的思路能帮你少走几步弯路。