
做个能被Agent真正用起来的技能系统我踩过的坑和最终方案都在这了过去半年我一直在折腾一件事怎么让LLM不只是一个聊天的模型而是一个能真正干活的Agent。聊过天的朋友都知道模型本身再聪明它也没法替你操作数据库、调用API、定时跑脚本。绕不开的核心问题就是技能Skills。无论是AutoGPT那套插件思路还是各种Agent框架里的Tool calling机制本质都是在做同一件事——给模型一双能干活的手。如果你正在搭自己的Agent或者用过LangChain、CrewAI这类框架但总觉得工具调用的设计边界模糊、技能多了之后管理混乱那这篇文章应该能帮上忙。我不会讲太多框架源码而是把如何从零设计一套能稳定运行、容易扩展、模型调用准确率高的技能系统用“为什么这么做”的方式讲清楚。1. 技能系统到底解决什么问题1.1 先厘清概念技能不等于API封装很多人一提Agent技能就觉得是写几个Python函数、加个装饰器就算完事。诚然这是最常见的形式但真正的难点在别处。我在早期就是这么干的定义了一个get_weather(city)函数再写一段prompt告诉模型“你可以调用get_weather”。结果模型经常不按预期的方式调用参数传错、忘记传参、甚至自己编造一个不存在的函数名。折腾了几天我发现问题不在函数怎么实现而在模型根本没法稳定地理解“什么时候用这个技能、参数到底怎么填”。所以技能系统的第一性原理不是把代码包起来而是把能力描述清楚让模型在“正确的时候”以“正确的方式”调用它。这也正是OpenAI后来在函数调用function calling里强调研“结构化描述”的原因。用个生活类比你去餐厅吃饭菜单上写着“宫保鸡丁 42元”你就能准确点单。但要是菜单只写“厨师拿手菜”服务员得反复跟你确认。Agent也一样技能描述不清模型就只能瞎猜。1.2 技能体系的目标边界一套称得上“系统”的技能方案至少要解决四个层面的问题缺一个后面都会补课层面核心问题典型表现描述层模型知不知道有这个技能、何时该用技能库有20个工具模型永远只调用其中3个参数层模型能不能把对话内容正确映射为参数用户说“明天北京天气”模型传了“北京”但日期传的是今天调度层参数非法/依赖缺失时怎么办多技能顺序如何决策技能A的输出要喂给技能B但模型跳过了A直接调B扩展层加新技能要改多少代码、会不会影响老技能加第21个工具后老技能调用率明显下降我见过的绝大多数“demo能跑、上线就废”的Agent项目基本都是在第一、第二个层面就出了问题第三个层面压根没考虑到。后面我会逐个拆开讲。2. 技能描述规范Agent能不能用对80%看这里2.1 描述的真实作用不是“给模型看”而是“帮模型判断”先说结论技能描述的最大价值不是告诉模型这个函数怎么实现而是帮模型完成两次判断——该不该用触发条件识别和怎么用参数映射与约束。写描述的时候我推荐一个比官方文档更实用的模板技能名称需要是动词短语能明确表达功能。避免调用“数据处理”这种含糊的名字 触发场景在哪些用户意图下应该调用这个技能举1-2个真实用户话术示例 参数说明每个参数的语义、取值范围、默认值、是否必填以及“从用户哪句话里提取” 返回值说明返回的数据结构大概是怎样的以及对“成功/失败”的判定标准 使用限制什么情况下不要用比如“仅查询、无写入权限”等。我曾经吃过一个亏给一个“查询订单”的技能写的描述是“根据订单号查询订单详情”。听起来没问题对吧但模型面对“帮我看看那个华为手机什么时候发货”这种口语化询问时就不知道怎么把“华为手机”映射成订单号因为描述里压根没说“订单号可以从用户提到的商品名、手机号、邮箱中模糊匹配”。然后在一次测试里模型自作主张把“华为手机”当作订单号传了进去查出来一个不存在的订单还一本正经地跟用户说“您的订单不存在”。后来改进的描述加了一句“订单号通常为12位数字用户可能不会直接给出订单号而是提供手机号、商品名称此时应优先调用search_order_id_by_info技能进行模糊匹配”。调用准确率立刻从60%多飙升到近90%。2.2 参数设计的三条铁律参数Schema是模型调用技能时最容易翻车的地方。实战下来命中这三条规则能规避绝大部分参数解析错误。规则一所有参数都给默认值能少让模型填就让模型填。模型不是人它不会主动向你确认“您要查询的日期是具体哪一天”它会自己猜一个。所以能通过上下文推断的参数比如默认查询今天的天气就设成可选参数、给默认值必填参数越少越好。规则二用枚举值和描述框死取值范围。如果你的接口只支持“chinese”、“western”那参数类型就不要定成string而应该直接给enum。模型在enum约束下几乎不会出错但自由文本下什么妖魔鬼怪都传得出来。规则三时间日期类参数一定强调解析规则。这是另一个重灾区。用户说“后天下午3点提醒我开会”模型在参数里传“后天下午3点”就完了压根没转成时间戳。很多技能失败不是接口问题而是参数格式问题。我的做法是在参数描述里明确写“请将用户表达的时间转换为ISO 8601格式相对时间以当前时间{{now}}为基准计算”。2.3 技能命名和分类这是被严重低估的隐性坑当技能数量少于10个时命名问题几乎不会暴露。但一旦技能超过15个模型选择技能的准确率会肉眼可见地下降这时候命名和分类的价值就出来了。我的具体做法是统一按“动词宾语”命名如query_order_info、send_email_notify让模型一看名字大概知道干什么禁止含义重叠的命名。比如不能同时有get_user_info和fetch_user_profile这会让模型选择困难按业务域拆分命名空间技能名里带上域前缀如order_query_status、order_create_returns、user_get_vip_level。有前缀的好处是即便模型不完全确定技能名通过前缀匹配也能找到“看起来对”的那一类返回统一格式每个技能返回{success: bool, data: object, message: string}结构这会让上层agent的逻辑处理简单一个数量级。3. 技能引擎的落地实现从“能用”到“好用”3.1 注册中心把技能当成可插拔组件既然叫系统就不能靠散落的函数。我的建议是把每个技能做成独立的可注册组件用一个全局注册表管理。一个比较通用的实现# skill_registry.py from typing import Callable, Dict, Any from pydantic import BaseModel class SkillSpec(BaseModel): name: str description: str parameters_schema: Dict[str, Any] returns_schema: Dict[str, Any] | None None enabled: bool True handler: Callable None # 实际执行函数 class SkillRegistry: _skills: Dict[str, SkillSpec] {} classmethod def register(cls, spec: SkillSpec): if spec.name in cls._skills: raise ValueError(fduplicated skill: {spec.name}) cls._skills[spec.name] spec classmethod def get(cls, name: str) - SkillSpec: return cls._skills.get(name) classmethod def list_skills(cls) - list[dict]: return [ { name: s.name, description: s.description, parameters_schema: s.parameters_schema, } for s in cls._skills.values() if s.enabled ]注册中心的好处是你可以在一个集中位置查看所有已注册技能、动态启停某个技能、甚至给每个技能标注版本。调试模型乱选技能时直接去注册表扫描描述是否清晰效率极高。技能的物理组织上我会用“目录即模块”的方式skills/ ├── weather/ │ ├── __init__.py # 注册逻辑 │ ├── describe.yaml # 技能描述与参数schema │ └── handler.py # 实现代码 ├── order/ │ ├── __init__.py │ ├── describe.yaml │ └── handler.py每个技能目录自带描述文件和实现代码互不干扰。新增技能时就复制一个目录改改不需要改动主程序。这对我这种懒人来说是刚需。3.2 技能描述的结构化不用纯文本拼prompt描述文件统一用YAML是为了让代码能“结构化工读”技能信息方便做离线检查。下面是我业务里常用的describe.yaml结构name: query_order_info description: 查询订单的状态、物流、商品明细等基础信息。 当用户询问“订单到哪了”“什么时候发货”“我的订单还在不在”等问题时优先调用。 enabled: true parameters: - name: order_id type: string required: true description: 订单号12位数字。若用户未直接提供可先用 search_order_by_info 模糊匹配。 - name: include_items type: boolean required: false default: false description: 是否返回订单下的商品明细默认不返回。 returns: type: object properties: status: { type: string, enum: [pending, paid, shipped, completed, cancelled] } logistics: { type: string, description: 物流公司及单号 } amount: { type: number }这套YAML最终会喂给模型但更重要的是我会用它做“技能质量检查”所有必填参数数量≤3、所有描述长度在50200字之间、名称不重复等写个CI脚本自动跑一遍。质量不合格的技能根本不允许注册。3.3 让Agent决定“调不调、调哪个”的决策循环只把技能描述一股脑塞给模型是不够的必须有明确的决策循环。我使用的是带反思的ReAct轻量变体核心步骤系统层先把“当前技能列表”拼进system prompt模型根据用户问题生成内部推理并输出“调用某个技能”的结构化动作引擎执行技能返回结果模型读取结果判断是直接回答用户还是继续调用下一个技能若连续两步没有调用技能直接结束循环输出最终回答。伪代码长这样def run_agent(user_query: str, registry: SkillRegistry, max_steps5): messages [ {role: system, content: build_system_prompt(registry.list_skills())}, {role: user, content: user_query}, ] for step in range(max_steps): response llm.chat(messages, response_formatjson) action parse_action(response) # {skill: ..., params: {...}} if not action[skill]: return response[final_answer] result registry.get(action[skill]).handler(**action[params]) messages.append({role: tool, content: json.dumps(result, ensure_asciiFalse)}) return {error: max steps reached}有个细节值得强调中间结果一定要完整地写回messages。有些朋友为了省token只把执行结果的一小段摘要喂回去结果模型丧失了对全局的感知很容易在前置技能输出不全的情况下二次调用错误技能。token该省的地方是历史消息压缩不是工具结果。3.4 记忆层让技能学会“复用”这个问题比较进阶但非常影响体验。同一个Agent用户昨天问了商品价格今天又问“那上次那个商品呢”如果技能执行时没有记忆上下文就答不上来。我的方案是给技能引擎加一个“短期记忆槽”每次会话中技能A的返回结果会根据Append-only模式写入一个会话变量区当模型解析用户问题时会先检查变量区里是否有相关实体再决定是否需要重新调用技能去拉数据。例如上次查询的商品ID保存在session.entities.product_id里这次用户说“那个商品”Agent会优先把这个ID传进查询技能而不是再去搜索引擎找一遍。注意这里面有个权衡哪些信息该进记忆槽、哪些不该我的经验阈值是——能被复用的、跨技能共享的、需要额外API成本才能获取的才存。像一次性的“今天天气”就不值得存。4. 常见问题与排查技巧实录技能系统上线后会不断遇到各种“模型不好好干活”的情况。这里整理几个高频问题和我验证过的排查方案建议收藏当手册用。高频问题可能原因排查与解决方案技能存在但模型从不调用技能名/描述里有词汇和用户口语不一致描述太长被截断技能没有出现在System Prompt里打开注册中心的“技能列表预览”看模型实际看到的文本是什么用5种不同表述的用户问题做回归测试模型调用了错误技能多个技能描述存在语义重叠命名不具区分度把重叠技能合并或拆分检查命名是否都遵循“动词宾语”给描述加“何时不要用它”参数解析错误严重Schema过宽松缺少示例值模型无法从口语中提取实体必填参数加枚举给时间日期类参数加基准时间写死常见别名如“手机”“电话”都映射为phone技能执行报错后Agent直接摆烂错误信息未结构化返回模型不知道下一步怎么办错误统一返回{success: false, message: ...}在System Prompt里加“当技能执行失败时应尝试换一种方式或向用户说明不要假装成功”技能多了之后整体准确率下降决策空间变大描述质量参差不齐对技能做分群高频技能放开、低频技能收进二级菜单/子技能组启用“兜底技能”作为模型不确定时的唯一退路4.1 踩坑实录一模型“看到了”技能但总不用有一次排查一个“内容摘要”技能死活不生效的问题。我检查了注册表技能在列表里描述写得也算清晰用户明说“帮我总结一下这篇文章”模型偏偏不调技能而是自己用大模型能力现场总结。后来我把system prompt完整打出来看了一眼发现技能列表被拼在系统指令最末尾而我的系统提示词长得离谱模型在处理长上下文时注意力天然地偏向中间靠前的部分末位技能被“视而不见”。解决办法很简单把技能列表的位置前移到用户消息之前、系统指令之后并把当前最可能用到的3个技能动态置顶。改动一天后调用率从不到40%提到了80%左右。这个案例说明prompt工程里“内容位置”这个微妙因素在技能系统里被放大得特别明显。4.2 踩坑实录二模型“参数幻觉”与重复调用另一类高发问题是模型在参数里填了“自己以为的值”。比如用户说“我看下北京天气”技能调用没问题但模型在参数里传了city: 北京结果查询API要的是拼音或城市代码。这种问题靠模型提示收效甚微最有效的办法是在参数Schema的description里直接给一个sampledescription: 城市拼音全拼小写例如beijing、shanghai、guangzhou。不要用中文或英文全称。加了sample之后准确率提升非常明显因为模型对“格式示范”的敏感度远高于抽象描述。这也侧面验证了一个观点给模型一枚参数胜过给它一段规则。还有个高频问题模型调用一个只读技能时因为返回超时它居然自动重试了三遍。我一开始以为是重试逻辑的问题后来发现是引擎把“超时”当成普通错误返回给了模型模型看到错误信息就自作主张地重试。解决方式是让超时错误返回一个retryable: false的标记并在描述中明确“超时不代表失败不要重试”。4.3 排查工具建议日志比可视化界面更实用很多人会给技能系统配一个可视化界面看Agent一步步调用了什么。但我实际体验下来调试阶段最好用的是结构化的日志每一行记录{ timestamp: ..., turn: 3, action: call_skill, skill_name: query_order_info, params: {...}, result_status: success, latency_ms: 320, message_snapshot: ... }然后去统计每个技能的调用频率、成功率、平均延迟、参数解析出错率。用数据而不是肉眼去看Agent“做了什么”能快速定位是哪个技能拖了后腿。我做过一个自己的技能健康度看板每周跑一次四个关键指标异常即告警。这个做法救了我很多次。5. 技能系统的边界与后续演化方向5.1 一个技能系统做不到什么我得说点泼冷水的话。再完善的技能系统也藏不住一个脆弱的能力边界。总结起来以下问题不是靠“调参”能解决的技能依赖的外部服务不稳定比如上游API限流、响应慢再好的技能编排也白搭复杂多跳任务里Agent经常在第二步选择错误技能导致整个链路断裂。当前主流方案是靠“一次性生成完整执行计划”即Plan-and-Execute但这对技能描述的粒度要求更高系统复杂度呈指数上升故障恢复能力有限。一个技能执行到一半挂掉目前多数框架很难做到“从失败点续跑”最现实的处理是让Agent识别失败并整体重试或者明确告知用户部分能力暂不可用。5.2 两个我认为值得投资的方向我自己在走的两条路一是技能组合与流程模板。把高频的多技能调用链沉淀成模板比如“客服查单→修改地址→触发短信通知”这种模板化的编排往往比让模型自由发挥稳定得多。本质上是在技能调用之上加了一层“流程层”牺牲部分灵活度换取确定性。另一个是技能的自我学习/进化。这个方向比较新怎么把一次成功的多技能调用经验固化为一个新技能仍没有成熟的做法。但我的观察是凡是能在生产环境稳定跑的Agent几乎都离不开“人工沉淀技能模型调用”的混合模式。所以如果你打算做技能系统的长期演化第一批技能不要指望模型自动生成老老实实靠业务专家来定义和打磨。回到标题“agent-skills”我现在的理解是这个领域的核心矛盾不是“模型能力不够”而是“能力描述、注册、调度、演进这套工程体系跟不上模型的发展”。技能系统在设计之初就要想清楚它是要承接一个快速演进的能力库而不是一个固定不变的函数集合。我个人的经验是先小范围验证35个核心技能把描述质量、参数规范、监控体系打磨顺再根据用户真实询问逐步拓展。不要一上来就铺几十个技能展示“我们很强大”大概率会连基本的调度准确率都保不住。最后分享一个操作上的小技巧每次新增技能时我会强制自己先写5条“用户可能怎么问”的真实表述然后拿这5条去测模型是否能准确触发新技能、不会误触老技能。这个习惯陪我避开了绝大多数上线后的“模型乱选技能”事故。准备搭建你自己技能系统的朋友建议也把这步变成固定动作。