最近一直在折腾 agent-skills 相关的工程化方案说白了就是琢磨怎么让大模型从“只会聊天”变成“能稳定干活”。这半年下来踩过的坑、推倒重来的设计都不少。今天不写抽象理论把我实际用下来的一套技能体系设计思路、关键实现细节以及几次印象深刻的排障经历一次性整理出来。1. 为什么需要“技能”这层壳整体设计思路拆解1.1 从“会聊天”到“会干活”的断层先聊一个很现实的场景。你让模型直接调用一个天气 API它可能生成了接口地址但参数格式错了你让它查数据库它三天两头编造字段名你让它连续多步操作它经常在第二步就把前面拿到的数据丢得干干净净。这些问题不是模型“笨”而是你给了它一个裸模型却没给它可以稳定依赖的工具抽象。我做 Agent 应用的一条铁律是模型负责决策工程负责执行。而技能层正是“决策”和“执行”之间的那个接缝。大模型本质上是个超大型语言模型最擅长的是预测下一个 token。你问它问题它给你一个看起来合理的回答可一旦让它去完成“查询订单状态—核对库存—生成发货单”这种多步骤任务每一步都涉及外部系统交互每一步的返回结果都是实时变化的。这时候如果所有逻辑都堆在提示词里模型既要记业务规则又要记工具用法还要处理各种返回数据上下文稍微长一点就开始“放飞自我”。我在实际项目里见过最夸张的一次模型把接口返回的订单号当成了库存数量去参与计算——因为它根本不知道自己调用的函数返回了什么语义。说白了裸模型拿到的是一堆孤立函数的字符串描述它对这些函数之间的逻辑关系和业务含义一无所知。技能这个概念的引入就是要把这些碎片化的函数调用、业务规则、数据处理逻辑组织成一个个有明确边界、有清晰语义的“独立工种”。1.2 技能的本质把不确定装进确定所以我最终把技能skill看作一个“封装单元”它把三样东西绑在一起一段能被模型理解的功能描述、一组结构化入参出参定义、一段可执行的业务逻辑。模型不需要知道技能内部怎么实现它只需要根据描述判断“要不要用这个技能”然后把参数填对剩下的交给技能本身去执行。这个设计最大的好处是把不确定性关进了笼子里。模型的自由度被严格限制在“选技能、填参数”这两件事上而这两件事恰恰是模型相对擅长、也最容易用工程手段去约束的。技能内部则是完全确定性的代码连接数据库、调用 API、处理文件、做计算每一步都是可预期的、可测试的。你可能会说这不就是把一个函数包了一层壳吗确实但这一层壳决定了模型能不能正确找到你写的函数。一开始我也觉得奇怪“不就是给函数写了段注释吗为什么效果差别那么大”后来发现问题根本不在“有没有注释”而在“描述的质量”。传统编程里函数名和注释是给人看的人本身有常识和上下文能猜个八九不离十但在 Agent 体系里这段描述是给模型看的模型没有任何隐性上下文它只能靠你写在 description 里的字面信息来做判断。同样的功能你写“获取天气信息”和写“根据城市名称返回实时天气及未来三天预报当用户询问天气相关问题时使用”后者在实测中的调用准确率能高出二三十个百分点。这二十个百分点就是工程化设计带来的直接收益。1.3 一个通用技能框架的模块划分我沉淀下来的技能框架最终分成四个层次技能描述层、技能注册层、技能执行层、技能运行环境。技能描述层就是上面说的元数据包括技能的唯一标识、简短的功能描述、入参出参的 Schema。这一层面向模型核心目标是降低调用的误判率。技能注册层维护一份“技能清单”相当于公司花名册模型需要某个能力时先来这里查一查确认技能存在且可用。技能执行层是真正干活的部分接收已经校验过的参数调用外部工具返回标准化结果。技能运行环境则负责资源隔离、超时控制、日志采集这些“不那么性感、但少一个就出事故”的底层能力。这四个层次从设计上就是松耦合的你可以在描述层不断调优措辞完全不影响执行层代码你也可以替换执行层的实现从模拟数据切到真实 API而不需要改描述。我经常跟伙伴们说技能体系的设计目标不是“写出一堆函数”而是“让技能像乐高积木一样可以拼装、替换、扩展”。当技能数量多起来之后这种模块化的好处会指数级放大——不是每个新功能都要从零开始写代码而是从现有技能中挑选、组合、微调效率完全不一样。2. 技能设计与核心实现细节2.1 技能元数据你给模型看的“说明书”这是整个技能体系的灵魂。我把技能元数据设计为统一的 JSON 结构name技能唯一名称建议用“动词_对象”的格式比如 search_documents、cancel_order。description一段自然语言描述模型靠它判断是否调用。parametersJSON Schema 格式的入参声明。returns出参结构声明。timeout超时时间属于运行层信息。version技能版本号。技能命名的规范不是小事。技能名会被模型作为工具名填入调用请求里命名越直观模型选错的概率越低。我早期吃过亏把一个“订单取消”技能命名为 order_cancellation_v2_quick结果模型在用户要求“退单”的时候反复挑选其他名称更贴近的技能最后才勉强选中它。改成 cancel_order 之后调用率立刻上来了。从这里我学到一个经验给技能命名要假设模型对业务一无所知它只能从字面意思里猜用途名字就是第一道引导。description 的写法也有一些经过验证的技巧。我总结出三个要点第一开头第一句话直接说明这个技能是干嘛的不要铺垫第二列举典型的触发场景给模型提供判断锚点第三如果技能有适用边界一定要写清楚“不处理什么”避免模型滥用。比如做摘要技能的描述可以这样写“对给定的长文本生成不超过 500 字的中文摘要。适用于会议纪要、新闻文章、政策文档等长文本的要点提炼。不适用于问答、翻译、关键词提取等任务。”这样写模型在“帮我总结一下这个会议纪要”时就会优先想到它而在“这段翻译成英文”时就不会硬套它。2.2 参数声明与校验把坑挡在进门之前参数设计上我强制使用 JSON Schema 来声明入参。很多人觉得这一步多此一举反正模型生成的参数合不合法执行的时候验证一下就行。但我实测下来参数校验前置化是成本最低、收益最大的优化方式。因为模型在生成调用请求的时候会参照你提供的 Schema 来构造参数。如果你只写“params需要城市名”模型可能传 city可能传 location还有可能把“北京”和“北京市”同时传进来但如果你声明了 enum 或者明确的 type 和 description模型生成参数的合规率会显著提升。JSON Schema 里有两个字段特别值得重视一个是 description写在每个字段上给模型详细解释这个参数应该填什么另一个是 required明确告诉模型哪些参数是必须的。举个例子参数名 city城市名称使用标准中文名例如“北京”“上海”必填。参数名 days预报天数可选整数取值范围 1 到 7。实践下来这种带语义的 Schema 声明比“宽松入参、执行时兜底”的方案稳定得多。兜底逻辑还是要写的模型毕竟不保证 100% 遵守规则但要把它当成“最后一道防线”而不是“主要依赖”。校验返回的错误信息也要设计得对模型友好比如“参数 city 必须为字符串类型当前收到 int 类型”模型看到这条信息后下一轮往往就会自我纠错重新生成正确的参数。2.3 技能执行体的分层实现技能执行体建议按三个层次去写入口层、校验层、业务逻辑层。入口层做的事情很少接收模型传过来的 JSON 参数把参数转换成内部对象。校验层负责对参数做严格的合法性检查类型不对就抛异常返回给模型而不是让它一头扎进业务逻辑里。业务逻辑层则是真正干活的地方调用 API、访问数据库、组装返回结果。这里有个容易忽略的点业务逻辑层返回给模型的结果不一定是原始的 API 返回。模型能理解的是文本和结构化数据但它对超长 JSON 的解析能力是有限的。所以执行体最后要有一个“结果归一化”处理把原始返回精简成模型决策所需的最小信息量。比如调用一个查询天气的 API原始返回可能有湿度、风速、气压、紫外线指数、降水概率等二十多个字段但模型下一步可能只需要“今天是否下雨”。那就直接在技能内部把结果压缩成“今天有雨建议带伞”这样的中文短句模型拿到之后直接就能用不需要再从一团 JSON 里翻找、理解、转述。我一直强调一个观点技能的输出不是给程序看的是给模型看的。程序可以直接消费原始 JSON但模型不行。它需要的是“语义浓缩后的反馈”而不是“结构完整但信息庞杂的数据转储”。2.4 技能组合与编排思路单技能只是第一步真正的 Agent 应用需要技能协作。我的编排思路是把技能连接成“技能链”模型在多步任务中依次调用不同技能前一个技能的输出作为后一个技能的输入。这个思路在实际落地中有两个重要注意点。第一技能间的数据流要显式传递不要在技能内部藏着全局变量。模型在多步操作中需要看到上一步的结果才能做出下一步决策如果数据被藏在技能内部的黑盒里模型看不到它就无从决策。第二每步返回给模型的信息都要标注“这一步做到了什么、拿到了什么”让模型清楚自己当前处于整个任务链的什么位置。否则模型很容易在前两步结果之间迷失最后输出一个张冠李戴的报告。我还尝试过把编排逻辑放在“技能调度器”里由调度器决定调用顺序而不是完全交托给模型。这种方式适合流程固定的场景比如“先查库存再生成发货单”而完全开放的方式适合探索型任务比如“帮我看看这个领域有什么最新研究进展”。两种模式各有适用场景一个成熟的 Agent 系统里两者应该并存——用调度器兜住固定流程的下限用模型的自由发挥兜住开放性任务的上限。3. 实操从零到一实现一个可用的技能3.1 需求拆解与技能选型用一个实际例子来演示。某团队想做一个“自动检索资料并写简报”的 Agent用户给它一个主题它需要完成三件事搜索相关资料、从资料中提取要点、按简报格式输出。这个需求看起来不复杂但拆解之后会发现如果写成“一个技能干所有事”技能的 description 会非常冗长模型反而不知道从何下手。正确的拆法是把任务切成三个子技能search_info根据主题检索资料返回资料列表及摘要。extract_key_points对给定文本提取关键要点返回要点列表。generate_brief基于要点列表生成简报。三个技能职责单一、边界清晰模型在多步任务中会依次调用每一步的返回都只包含下一步所需的信息整个链路非常干净。选型时我还有一个判断标准如果某个技能的 description 超过三句话才说清楚就该考虑拆分了。一个技能只做一件事是技能设计的第一原则。3.2 写一个“资料检索并生成摘要”技能下面给出一个简化的 search_info 技能实现用 Python 写。需要说明的是这个示例把外部 API 调用部分做了抽象实际接入时换成真实搜索服务的接口即可。import json import requests from jsonschema import validate class SearchSkillError(Exception): pass class SearchInfoSkill: name search_info description 根据主题检索相关资料并生成摘要列表。适用于需要获取特定主题背景信息、文献资料、行业动态等场景。 parameters_schema { type: object, properties: { topic: { type: string, description: 需要检索的主题关键词例如神经网络压缩、碳达峰政策, }, limit: { type: integer, description: 需要返回的检索结果数量默认5最大10, minimum: 1, maximum: 10 } }, required: [topic] } def execute(self, params: dict) - dict: # 参数校验 try: validate(instanceparams, schemaself.parameters_schema) except Exception as e: raise SearchSkillError(f参数校验失败: {str(e)}) topic params[topic] limit params.get(limit, 5) # 估算权重此处以模拟数据代替真实检索服务 results self._search(topic, limit) # 结果归一化只保留模型决策所需要的信息 normalized [] for r in results[:limit]: normalized.append({ title: r[title], summary: r[summary], source: r[source] }) return {status: success, results: normalized} def _search(self, topic: str, limit: int) - list: # 真实场景中接入搜索服务 API示例仅返回模拟数据 mock_data [ {title: f{topic}相关研究综述, summary: 本文概述了该领域的关键进展与核心争议点适合作为背景阅读。, source: 示例库A}, {title: f{topic}技术实践报告, summary: 从工程角度介绍了落地实现方法、性能评估与注意事项。, source: 示例库B}, {title: f{topic}行业趋势分析, summary: 总结了近期行业动态、主要参与者与未来趋势判断。, source: 示例库C}, ] return mock_data这个示例里有几个设计细节值得说明。先看名称和描述技能名 search_info 很短description 里写了“检索相关资料并生成摘要列表”并列举了两个典型的触发场景。模型在 function calling 机制中会把这个描述作为一个候选工具去匹配用户意图描述写得越贴合匹配就越准。再看参数 Schematopic 是必填的limit 选填且有范围限制。JSON Schema 的 validate 函数在 execute 入口直接对参数做校验如果模型生成参数不合法会直接抛错而不是带着非法参数去访问外部接口。最后看结果归一化真实搜索接口通常会返回非常多字段但我只取了 title、summary、source 三个字段。这三个字段已经足够模型进行下一步“提取要点”了而且能有效降低上下文噪音。这一步是我在实际运维中反复优化过的——一开始我偷懒直接把原始返回扔给模型结果模型经常被一堆无关字段干扰生成的报告质量忽高忽低。改成精简输出之后稳定性明显提升。3.3 注册技能并接入 Agent 主循环有了技能类之后下一步是把它注册进技能清单。我维护一个全局技能注册表启动时扫描并加载所有技能这样 Agent 在运行过程中可以随时查询有哪些可用技能。skills_registry {} def register_skill(skill_cls): instance skill_cls() skills_registry[instance.name] instance return instance def get_all_skill_definitions(): return [ { name: skill.name, description: skill.description, parameters: skill.parameters_schema } for skill in skills_registry.values() ]Agent 主循环里模型会收到“目前可用技能列表”根据用户的自然语言指令决定调用哪个技能。调用时Agent 框架把模型生成的目标技能名称和参数传给对应技能实例def call_skill(skill_name: str, params: dict) - dict: if skill_name not in skills_registry: return {status: error, message: f技能 {skill_name} 不存在} skill skills_registry[skill_name] try: result skill.execute(params) return {status: success, result: result} except Exception as e: return {status: error, message: f技能执行异常: {str(e)}}这里有一个容易忽略的细节调用技能之后返回结果一定要给模型一个“执行反馈”包括成功或失败的标记。很多初版 Agent 应用把技能内部异常直接吞掉模型看不到执行结果后续步骤就会基于错误的状态继续“演下去”最后给你一个看着很完整、其实全错的结果。我最开始做时也犯过这个错直到有一次助理报告“报告里引用的数据明明不存在”一查日志才发现技能调用早就异常了但模型硬着头皮编了下去。后来所有技能调用统一走这个对模型可见的 wrapper情况才好转。3.4 让技能“长”出记忆与状态纯函数式的技能适合无状态操作但很多实际场景需要技能在多次调用之间保存状态。比如一个“任务管理器”技能用户先让它“记下明天下午三点开会”再让它“提醒我明天的安排”两次调用之间需要共享同一个任务列表。我的做法是给技能实例增加一个可选的记忆区每个技能可以持有自己的状态字典。为了保证状态不失控我规定技能的状态必须能在每次对话结束后序列化保存下次会话时重新加载。这样技能既是“一次性的执行器”也是“持久的记忆体”。但这里我也要提醒一句状态管理会增加复杂度和出错面。我的建议是默认情况下技能做成无状态只有当功能明确需要跨调用共享信息时才引入记忆。尤其在多技能场景下内存归属问题很容易踩坑。假设 search_info 和 generate_brief 都用到了“上一轮检索结果”把这个信息放在哪个技能的记忆里都会造成逻辑混乱。我最终采用的方案是所有技能共享一个 Agent 上下文缓冲区技能间的数据传递通过这个缓冲区进行而不是通过技能内部状态。每个技能只负责“读入参数、产出结果”至于结果要不要被下一个技能使用由 Agent 主调配逻辑去决定。这样虽然牺牲了一点“技能自治性”但换来了整体流程的可控性。4. 疑难问题与排查技巧实录4.1 模型总是不调用技能怎么办做 Agent 应用最先遇到的问题基本都是一样的你辛辛苦苦写好了技能但模型在对话里绕过技能直接凭“想象”回答。排查这类问题第一步不是改代码而是看日志里模型到底有没有看到技能定义。有些框架会把技能列表传给模型但出于上下文裁剪策略某些描述被截断了——模型压根不知道有这个技能自然就不会调用。这种问题一查对话开始阶段的上下文内容就能发现。第二步是检查 description 的触发场景是否清晰。我遇到过比较典型的情况技能的 description 写的是“获取用户信息”但模型面对的问题是“查一下这个用户的 VIP 等级”语义上匹配不上。把 description 改成“查询用户的会员等级与权益信息当需要了解用户级别、权益时使用”之后调用率立竿见影地提升了。第三步也是很多人漏掉的一点检查技能名是否太“抽象”或者太“相似”。当技能库超过十个技能名的辨识度就变得极其重要。如果你有两个技能分别在名字里带 order 和 orders模型会很容易混淆哪个对应哪个业务场景。4.2 技能执行结果“因果错位”的排查有一次团队在调试一个多技能 Agent 时发现模型明明调用了 A 技能获取用户列表但最后输出的报告里引用的数据来自 B 技能的返回。这种“因果错位”问题根源往往不在模型而在上下文管理。当模型执行多步调用时Agent 框架会把每一步的工具返回结果一股脑地拼进对话历史。如果返回结果没有来源标识模型分不清哪个结果对应哪次调用尤其当多个技能返回结构相似的 JSON 时很容易张冠李戴。解决方法是在返回结果的文本里自带“来源标识”。我们给每个技能的结果前缀加了一步编号比如“【步骤2-查询库存】库存数量120件”。模型在生成后续内容时就有了明确的指代锚点。这个改动很小但在排障上省了大量时间——不只是让模型少犯错更关键的是出问题时你能一眼从对话历史里看出哪一步开始错了而不是整段推倒重查。4.3 长技能链的稳定性优化技能链超过三步之后模型的失误率会明显上升。我总结出三个有效的优化手段。第一上游技能的返回结果要“尽量薄”只留下游技能真正需要的信息减少无关数据对模型的干扰。这需要在设计技能时就想清楚“这个结果会被谁消费、消费什么字段”而不是先把所有数据都倒上去等出问题再删。第二在 description 里为每个技能指明它在任务链中的角色。比如“该技能通常在其他技能获取原始数据后调用”。模型看到这种提示会更倾向于遵循合理的调用顺序。我把这理解成给模型一张“流程地图”它不是万能的但能让模型在两眼一抹黑时有方向。第三对特别复杂的任务不要指望模型一次规划到底。要把大任务拆成多个子任务在 Agent 主循环里串行执行每完成一个子任务就向用户反馈进度。这既是为了降低单次推理的复杂度也是为了让用户能在任务执行的中间阶段介入纠偏——这一点在实际产品体验里非常重要。搭配这几个手段我自己项目的多步任务成功率从早期的大概六成逐步稳定到九成以上。这里说的成功率是“任务完整执行完成且结果可验证”的比例。可见工程化手段对 Agent 应用稳定性的提升比换一个更大更强的模型要明显得多而且成本更低。4.4 多技能并发与状态隔离最后聊聊并发。当 Agent 同时响应多个用户请求时技能实例是否线程安全就成了必考题。我的做法是技能类本身做成无状态单例任何用户级别的数据都不存在技能实例上如果有需要保存的用户数据放到按 conversation_id 隔离的会话存储中。这样多个用户并发执行同一个技能时不会互相污染。更具体地说我给每个会话分配一个独立的上下文 key所有需要持久化的数据都挂在这个 key 下面技能执行时按需读取。在代码层面不要在技能类里定义可变的类属性也不要在 execute 方法里使用全局变量。看起来简单的几条约束在并发压测下能避免大量数据串扰问题。我早期就吃过亏两个用户同时让 Agent 查询各自的订单结果 A 用户的结果里混进了 B 用户的订单信息。排查后才发现技能类里放了一个用户级的缓存字典两个请求的读写互相蹿了。把缓存改成会话级隔离之后问题彻底消失。从那以后我给自己定了一条规矩技能设计阶段就把“并发安全”写进 checklist而不是等线上出问题再补救。我在实际把这整套技能体系落地到项目之后最大的感受是当技能数量超过十个最大的瓶颈反而不是模型能力而是技能设计的“元能力”。你得不断琢磨怎么给模型写清楚说明书怎么把结果归一到最简怎么把状态隔离干净。这套能力一旦建立起来后续加新技能就像填表格一样简单——写描述、定 Schema、写执行体、注册四步就完事。如果大家正在做 Agent 应用我的建议是先把一个技能做到极致再考虑扩展成技能库。一个描述精准、参数严谨、结果干练的技能比十个描述含糊的技能加起来更能解决实际问题。技能从来不是越多越好而是每个技能在模型眼里都“一眼可懂、一调就准”这才是 agent-skills 这套设计真正值钱的地方。