做AI Agent开发的人这两年应该都有一个共同的体感模型能力越来越强但真正让Agent“干活”的部分反而越来越成为瓶颈。你给模型一堆工具函数它调用起来总是差那么点意思——要么参数传错要么流程走偏要么遇到边缘情况就卡死。这背后的核心问题不是模型不够聪明而是我们缺少一套真正能被模型稳定理解和执行的“技能体系”。我最近在整理一个叫agent-skills的项目方向就是专门解决这个问题把零散的工具调用升级成一套结构化的、可组合、可复用的Agent技能系统。如果你正在做Agent应用、工作流编排或者单纯对“怎么让大模型稳定地完成复杂任务”这件事感兴趣这篇文章值得你花十分钟读一下。我会把项目从设计思路到落地细节配合实操踩坑经验一次性讲清楚。1. 项目整体设计与核心思路拆解1.1 为什么需要专门的技能体系先看一个常见的痛点。早期我做Agent的时候习惯把功能直接写成一个个function然后塞进模型的tools参数里。比如一个客服机器人可能就有查订单、查物流、处理退款、联系人工等七八个函数。表面上很简单实际跑起来却是灾难函数之间没有依赖关系模型不知道应该先查订单还是先查物流。参数是扁平的JSON结构一旦业务复杂模型传参的准确率直线下降。遇到需要多步操作的场景比如“查一下我上个月买的手机发货了没”模型要自己拆解步骤拆错了整个流程就废了。agent-skills的核心思路是把这些零散的函数重新组织成“技能”。一个技能不再是一个孤立的函数而是包含意图识别、参数提取、执行逻辑、状态管理、异常处理的一套完整封装。模型面对的不再是一堆散装工具而是一个清晰的技能目录每个技能都明确知道自己该在什么条件下触发、怎么接收输入、怎么输出结果。打个生活化的比方普通工具调用像是给一个新手一堆家电遥控器每个遥控器上十几个按钮他得自己琢磨哪个是开空调哪个是调温度。而技能系统是给新手一份说明书告诉他“觉得热了就按这个键按完这个键再按那个键调温度”操作路径是预先编排好的。1.2 技能设计的三个核心原则做这套体系时我给自己定了三条原则现在回头看这三条原则基本决定了项目能否真正落地。第一技能必须声明式。每个技能要有一个完整的描述文件说清楚这个技能是干什么的、什么时候该用、什么时候不该用。这个描述不是给人看的是给模型看的。写得好不好直接决定模型能不能在正确的时候唤起正确的技能。我见过太多人在这儿偷懒技能描述就一句话“查订单”模型根本不知道怎么用它。第二技能必须可编排。单个技能解决单点问题复杂任务靠技能之间的组合。比如“处理退款”这个技能内部可能要先调用“验证用户身份”再调用“查询订单状态”然后才进入退款流程。这种编排关系要在技能设计时就考虑到而不是指望模型临时发挥。第三技能必须有容错机制。模型调技能不可能百分之百成功参数缺失、状态冲突、外部接口超时都是常态。每个技能都需要预先定义好失败分支比如参数不完整时触发反问澄清数据不存在时返回什么占位信息超时后如何降级。这块做得越细Agent在真实环境里的稳定性越高。这三条原则贯穿了整个项目的架构设计。接下来我具体拆解每一个核心模块。2. 核心模块解析与实操要点2.1 技能描述Schema的设计细节技能描述是整套系统的基石。我见过很多团队在这个环节草草了事最后都吃了大亏。一个合格的技能描述至少要包含以下字段name技能名称必须全局唯一且语义清晰。不要用“func_001”这种名字模型根本分不清。description技能描述包含触发条件和适用场景。这是模型判断要不要调用该技能的唯一依据。parameters参数声明每个参数都要写明类型、是否必填、约束范围、默认值最好附上示例值。returns返回值声明包括数据结构、可能的错误码。side_effects副作用声明比如是否需要写数据库、是否有金额变动。这一步很多人忽略但对模型决策很重要——如果技能会修改用户数据模型在回复用户时会更谨慎。我举个例子。一个“查询订单物流”的技能描述核心部分长这样name: track_order description: 查询用户订单的物流信息。当用户询问订单到哪了、物流状态、 发货了吗、快递进度等问题时调用。仅支持查询已支付订单。 parameters: order_id: type: string description: 订单编号格式为ORD开头的12位字符串 required: true example: ORD20250101001 user_id: type: string description: 用户唯一标识从会话上下文中自动获取 required: true auto_fill: true returns: data: status: string events: array error: code: string message: string side_effects: - read_only有几个细节值得强调。首先是auto_fill这个字段它表示参数可以从上下文自动提取不需要模型去问用户。这能大幅减少多轮对话的次数。其次是description里对触发条件的限定我把“仅支持已支付订单”写进去模型遇到未支付订单的查询请求时就知道不该硬调用而是回复用户先去支付。2.2 技能注册与模型调用的对接技能描述写好了还要有一套机制把它们注册到模型调用过程中。实际工程中我倾向于把技能注册做成一个配置驱动的流程。也就是说所有技能的Schema统一放在一个目录里程序启动时自动扫描、加载、校验最后生成模型需要的格式。一个典型的技术选型是把技能Schema转成模型的 function calling 格式。OpenAI、Claude、Qwen 这些模型虽然接口细节有差异但整体结构相似所以我做了一个适配层统一转成同一个内部schema对外再接不同模型的格式化器。这个适配层看起来简单实际能省掉很多重复劳动对接新模型时只需要写一个转换函数。校验环节同样重要。我有一次上线前没做严格校验一个技能的参数里写了required: TruePython风格而框架解析时用的是required: trueJSON风格结果该技能永远无法被正确触发。后来我加入了一套完整的Schema校验器每次启动时自动检查所有技能描述类型错误、缺少必填字段、默认值类型不符这些问题全都在进程启动阶段暴露出来而不是等到线上被模型调用时才出问题。2.3 技能编排引擎的状态管理当多个技能组合执行一个复杂任务时状态管理就成了最头疼的问题。比如“退换货”流程可能涉及身份校验、订单查询、售后单创建、退款计算等多个技能技能之间要传递数据还要共享同一个会话上下文。我的做法是把状态管理分成两层。第一层是会话级状态保存用户ID、当前会话目标、已经执行过的技能列表、中间产生的关键数据。这一层用内存缓存加Redis持久化允许Agent在多次调用之间保持上下文记忆。第二层是技能级状态记录单个技能执行过程中的临时变量、步骤进度和局部结果。这两层状态分离能有效避免跨会话串数据也能让单个技能的执行原子化——失败了可以重试不会污染整个会话。这里要特别注意一个坑不要把大对象塞进状态里。我第一次设计时图省事把整段业务中间结果存成了一个JSON字符串结果序列化和反序列化消耗了大量时间还容易出错。正确做法是只存关键ID和必要字段其他数据按需重新查询。3. 实操过程从零搭建一套Agent技能系统接下来分享一个完整的实操过程。为了避免空谈我以“搭建一个支持多轮对话的客服Agent技能系统”为例带你从项目初始化开始一步步走到技能执行验证。这套流程在我自己的项目里完整跑过你照着操作基本能复现。3.1 环境准备与项目初始化假设你已经有了一个基础的Python环境我用的是3.10首先安装核心依赖pip install pydantic pyyaml redis fastapi openai然后创建项目目录结构我习惯按模块划分agent-skills/ ├── skills/ # 技能描述目录 │ ├── track_order.yaml │ ├── refund_order.yaml │ └── check_user.yaml ├── engine/ # 技能编排引擎 │ ├── registry.py # 技能注册器 │ ├── executor.py # 技能执行器 │ ├── state.py # 状态管理器 │ └── adapter.py # 模型适配层 ├── actions/ # 技能对应的实际业务函数 │ ├── order_actions.py │ └── user_actions.py └── main.py # 入口文件一开始不要贪多先把一个最简单的技能跑通再逐步加复杂度。我当年直接搭建了十个技能结果调试时根本分不清是哪个环节出了问题后来老老实实退回到单技能验证。3.2 技能注册器的实现技能注册器的核心职责是扫描skills/目录下的YAML文件解析并校验后生成一个技能索引表。我使用Pydantic做Schema解析这样类型校验和默认值处理都能省不少事。# engine/registry.py from pathlib import Path from typing import Dict, Optional import yaml from pydantic import BaseModel, ValidationError class SkillParameter(BaseModel): type: str description: str required: bool False auto_fill: bool False default: Optional[object] None example: Optional[str] None class SkillDefinition(BaseModel): name: str description: str parameters: Dict[str, SkillParameter] returns: Optional[Dict] None side_effects: list [] class SkillRegistry: def __init__(self, skills_dir: str skills): self.skills_dir Path(skills_dir) self._skills: Dict[str, SkillDefinition] {} def load_all(self): for yaml_file in self.skills_dir.glob(*.yaml): with open(yaml_file, r, encodingutf-8) as f: raw yaml.safe_load(f) try: skill SkillDefinition(**raw) self._skills[skill.name] skill except ValidationError as e: print(f[Registry] 技能 {yaml_file.name} 校验失败: {e}) return self._skills def get(self, name: str) - Optional[SkillDefinition]: return self._skills.get(name)这里有个容易被忽略的细节auto_fill参数的自动填充逻辑。所谓“从上下文自动获取”不是框架自动做而是由注册器提供钩子函数从会话状态管理器里提取对应字段。比如user_id通常在对话开始时已经确认就可以注册一个resolver在调用参数填充阶段自动注入。这样模型在调用技能时就不需要显式提供user_id可以显著降低参数错误率。# engine/registry.py 追加部分 class SkillRegistry: def __init__(self, skills_dir: str skills): ... self._resolvers {} def register_resolver(self, param_name: str, func): self._resolvers[param_name] func def fill_auto_params(self, skill: SkillDefinition, context: dict) - dict: auto_params {} for param_name, param in skill.parameters.items(): if param.auto_fill: resolver self._resolvers.get(param_name) if resolver and resolver(context): auto_params[param_name] resolver(context) return auto_params一个常见的bug场景是resolver里返回了None但参数是必填的并且requiredTrue没关掉。我在早期线上事故中遇到过这种情况解决方法是确保auto_fill参数在定义时把required设为False真正必填的校验通过fill_auto_params完成手动校验。3.3 技能执行器的设计思路技能执行器是Agent调用技能的“夹层”模型产出结构化调用请求执行器解析请求填充参数调用对应的action函数再把结果封装回模型能理解的结构。# engine/executor.py from typing import Any, Dict from engine.registry import SkillRegistry, SkillDefinition class SkillExecutor: def __init__(self, registry: SkillRegistry, actions_map: Dict[str, callable]): self.registry registry self.actions_map actions_map def execute(self, skill_name: str, params: Dict, context: Dict) - Dict: skill: SkillDefinition self.registry.get(skill_name) if not skill: return {error: {code: SKILL_NOT_FOUND, message: fUnknown skill {skill_name}}} # 自己补全auto_fill参数 auto_params self.registry.fill_auto_params(skill, context) merged_params {**auto_params, **params} # 参数校验 missing [ name for name, p in skill.parameters.items() if p.required and name not in merged_params ] if missing: return {error: {code: MISSING_PARAMS, message: f缺少参数: {missing}}} # 执行对应函数 action_func self.actions_map.get(skill_name) if not action_func: return {error: {code: NO_ACTION, message: f{skill_name} 没有绑定可执行函数}} try: result action_func(**merged_params) return {data: result} except Exception as e: return {error: {code: ACTION_ERROR, message: str(e)}}这里牵涉一个重要设计模型调用技能失败时的反馈回路。真实场景里模型不是一次就调对的。比如模型把order_id传成了orderId执行器要返回一个错误结构模型在这个错误结构里看到具体是哪个参数错了再重新发起调用。所以错误信息不能太笼统必须精确指出问题点这也是为什么我会把错误拆成SKILL_NOT_FOUND、MISSING_PARAMS、ACTION_ERROR等不同维度。模型看了就知道是该换技能、补参数还是该放弃。3.4 模型适配层的必要工作不同模型对function calling的格式要求不同所以适配层主要负责两件事Schema格式转换和模型输出解析。以OpenAI为例它要求工具描述写成这样的结构{ type: function, function: { name: track_order, description: ..., parameters: { type: object, properties: { order_id: {type: string, description: ...} }, required: [order_id] } } }适配层的转换逻辑就好比把内部统一的Schema“翻译”成各家模型能读懂的方言。不同模型对参数类型的支持精度不一样比如有些模型不接收number类型必须声明成string。好在各家都在往OpenAI的格式靠拢目前做兼容的难度比前两年小了很多。模型输出解析也有讲究。有些模型可能返回一个夹着注释的JSON或者把函数名写成了别名。我的策略是对模型的原始输出做三层容错——先原样解析失败则用正则提取函数调用片段再失败就把整个输出丢弃并返回“重新生成”的提示。这三层下来能覆盖绝大多数异常输出。4. 常见问题与排查技巧实录4.1 模型就是不调用技能怎么办这是新手最常遇到的问题。你给了模型一套很完善的技能库但它就是自己瞎编答案不触发任何技能。排查思路按优先级排列第一看描述质量。检查技能描述是否写清楚了“什么时候该用”。很多描述写得像给人看的比如“本技能用于查询订单”模型拿不准该不该用。更好的写法是直接把用户的意图映射到技能触发条件里用户在问物流、问进度、问发货状态就该调用这个技能。第二看参数示例。如果模型需要传入的参数比较模糊比如“订单号”模型不知道去哪拿可能会选择不调用。这时候把示例值和来源写清楚——参数是用户直接提供的、还是从历史消息里抽取的、还是需要反问确认的。第三修正few-shot示例。多数模型支持在system prompt里塞少量示例比如“用户问我的快递到哪了助手应调用 track_order(order_idxxx)”。这招效果立竿见影尤其对于复杂技能三个示例基本就能让模型稳定归类到正确触发路径。还有一个很阴险的坑模型有“过度自信”倾向。即使系统明确要求“必须调用技能才能回答”有些模型也会尝试直接生成答案。这时候要在system prompt里明确强调“如果你没有收到技能执行结果就不要回答任何业务信息”同时给执行器加一道防线——如果技能没有执行成功回复内容必须包含错误提示阻断模型的编造路径。4.2 技能参数反复提取错误参数提取是另一个重灾区。模型把日期格式传错、把数量单位传错、把用户意图里的间接信息漏掉这些问题几乎每天都能遇到。我常用的几个解决手段类型约束严格化。能在Schema里用枚举约束的就不要只写文本描述。比如订单类型只能传“普通订单”或“预售订单”就写成枚举模型的选择空间越小越不容易错。依赖自动填充。能从上下文拿到的信息用户ID、店铺ID、地区代码一律走auto_fill不给模型自由发挥的空间。模型能传错的参数越少整体准确率越高。回答前反问。如果某参数缺失且无法从上下文推断不要直接报错优先让模型用自然语言反问用户。比如缺少订单号可以让模型回答“请问您的订单编号是多少”用户提供后再触发技能。这样用户体验比直接报错好太多而且准确率更高。4.3 多技能编排时的顺序错乱复杂的任务流程涉及多个技能按顺序执行模型经常会把顺序搞反。比如“取消订单”应该是先查订单状态再执行取消模型可能直接调用了取消接口导致业务异常。解决方案有两个层面。第一尽量把编排放到技能内部而不是依赖模型现场决定。也就是说与其暴露cancel_order和get_order_status两个技能给模型不如暴露一个cancel_order_flow技能内部自己先查状态再取消。模型只需要做粗粒度的决策细粒度的编排交给代码控制。第二对于必须跨技能协作的场景利用执行器维护“技能调用历史”一旦发现某技能前置条件未满足直接返回错误并建议模型先调用前置技能。4.4 状态丢失与并发冲突当多个会话同时调用技能时状态管理会暴露出各种问题。最常见的是会话级状态被覆盖或者两个并发操作修改了同一份上下文数据。我的建议是给所有状态操作加上会话ID隔离。Redis里的key统一加上session:{session_id}前缀读写操作走同一个封装接口严禁在业务逻辑里直接访问原始状态。另外一个细节技能执行期间如果用到了外部资源比如数据库连接、文件句柄释放顺序要按照申请的反顺序来防止死锁。这些并发问题在测试阶段很难完全模拟最好是上线前列一个检查表逐项确认状态在不同异常路径下的清理逻辑。5. 进阶体验与效率优化5.1 技能编排的调试方法论调试Agent技能系统比调试传统后端服务困难得多因为错误可能发生在模型层、编排层或业务函数层。我的调试方法论是逐层隔离先验证业务函数本身没问题再验证执行器调用这个函数的结果符合预期最后才看模型是否正确触发了技能。具体做法是做两层日志一层记录模型与执行器的完整交互包括模型决策时的置信度、技能调用的入参和出参另一层记录技能内部的动作明细。这两层日志按会话ID关联出问题时可以先定位到哪一层再深入排查。实测下来这个习惯能把单个问题的定位时间从半小时压缩到五分钟。5.2 参数自动填充的高级玩法我们前面提到了auto_fill这里再展开讲一些进阶用法。除了从上下文填充user_id、session_id这种基础字段你还可以注册一些计算型resolver。比如用户查询“本月消费总额”resolver可以自动计算本月第一天的日期并填充给参数。这类逻辑放在resolver里比放在技能描述里更可靠因为它不依赖模型的语言理解能力。另一个思路是把resolver做成可组合的。比如有一个format_dateresolver负责把用户输入的自然语言日期转成YYYY-MM-DD格式另一个get_date_rangeresolver负责根据日期计算区间两个resolver可以串联使用也可以单独接入不同技能。组合一套resolver库技能参数的准确率和复用率都会显著提升。5.3 技能执行的可观测性设计一个成熟的Agent系统必须有完整的可观测性设计。我的结构是三个维度调用维度哪个技能、由哪个会话发起、执行了多久、状态维度当前会话处于什么阶段哪些数据已就绪、结果维度执行成功还是失败、失败原因分布。这三个维度的数据汇总以后既能用于排查问题也能反过来指导技能描述的优化。我举个具体例子。线上跑了一周后我发现某个技能报错的根因分布显示“参数缺失”占比最高。接着去翻会话日志发现这个技能依赖的一个参数从未被模型正确提取过。于是我把这个参数改成auto_fill从上下文中自动注入两周后这个技能的成功率从71%提升到94%。这种优化方式比瞎猜问题原因高效得多。6. 关于技能边界与系统稳定性的思考做技能系统时间长了我越来越觉得真正难的不是技术实现而是技能边界的划分。一个技能该多粗、该多细直接影响系统的灵活性、稳定性和模型的可控性。技能太粗一个技能干太多活内部逻辑复杂出错的概率就会上升而且模型对技能的理解成本也高。技能太细模型面对几十上百个技能选择困难编排成本飙升反而更容易选错。我目前的经验法则是一个技能对应一个用户可感知的业务意图。比如“退款”是一个技能但“退款”内部拆成“验证资格”“计算金额”“执行退款”几步这些步骤如果是稳定的先后关系就放在技能内部由代码编排不暴露给模型。只有当某一步本身是一个独立的、用户可能直接询问的意图时才把它提升为独立技能。另一个关于稳定性的思考是降级策略。任何依赖外部接口的技能都要有降级预案。比如查询物流信息如果第三方物流API挂了技能应该返回一个明确的“暂时无法获取”而不是报错退款技能如果支付通道异常应该转入人工处理流程而不是让模型硬编一个“退款成功”。这些降级逻辑必须在技能设计阶段就规划好等故障发生再想应对方案就晚了。我在实际项目里的体会是agent-skills这套体系的真正价值不在于代码多复杂、功能多花哨而在于它把Agent开发从“调模型碰运气”推进到了“工程化定义行为”的阶段。模型当然还有不确定性但通过精心设计的技能边界、参数约束、状态管理和容错机制你可以把不确定性的影响范围控制到最小把整套系统变成一件稳定可靠的工具。最后分享一个小的实操建议刚开始做技能系统时不要追求覆盖所有业务场景。挑一条最简单的链路比如“查订单状态—查物流—查退款进度”先完整跑通技能注册、模型调用、执行反馈这个闭环再逐步扩展。闭环能跑通你已经解决了Agent开发中最重要的一环。后续再慢慢加技能、调描述、优化参数系统会越来越顺手。