恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

从裸工具到技能系统:Agent 工程化实践与避坑指南

  • 首页
  • 资讯中心
  • /
  • 从裸工具到技能系统:Agent 工程化实践与避坑指南

相关资讯

金融行业AI转型:现状、应用与未来趋势 2026/9/19 23:04:31
open-code-review:一种可验证、可审计的AI代码评审范式 2026/9/19 23:04:31
Apache Atlas生产部署全记录:Hive血缘接入与元数据治理实践 2026/9/19 23:04:31

最新资讯

BrewUI:为Homebrew包管理器打造可视化图形界面
ESP8266与Arduino UNO串口调试实战:从AT固件到物联网接入
Edge同步机制深度解析:原理、断点与根治方案
对数-指数型模拟乘法电路:Multisim仿真与温度补偿设计
开源代码审查工作流:CLI+Git+LLM 实现可落地的自动化审查
OfficeCLI 打造工程蓝图风 Morph 演示:dark--blueprint-grid 风格从设计规范到可运行脚本

今日推荐

本周热门

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

从裸工具到技能系统:Agent 工程化实践与避坑指南

发布时间:2026/9/19 23:04:31
从裸工具到技能系统:Agent 工程化实践与避坑指南 如果让我用一个词来形容过去半年做 agent 应用最大的感受那就是模型本身越来越强卡脖子的问题全在agent-skills这套外围工程上。很多团队把大模型接进来之后第一版 demo 跑得飞起一上真实业务就露馅——今天的客服机器人反复调用同一个查询接口明天的数据分析 agent 把参数拼得驴唇不对马嘴。问题不在模型的推理能力而在我们根本没有给模型建立起一套清晰、可管理、可观测的“技能系统”。这篇文章想聊聊我在这条路上从混乱到规范化的完整过程。内容围绕如何设计技能的注册、声明、编排如何用代码把技能调度器落地以及我在真实业务里踩过的三个典型坑。适合正在做 agent 应用、被多工具调用问题困扰、想从“脚本调用”升级到“技能系统”的开发者参考。不是教科书是我自己的工程笔记。1. 从“工具调用翻车”到技能系统我为什么要重构 agent 的执行层1.1 那次让客服机器人原地打转的故障起因是一个客服问答机器人。它的功能很简单用户问订单状态模型调用后端接口查订单拿到结果后组织语言回复。初版只接了三个接口模型表现还不错。后来业务扩张接口从三个涨到十几个问题开始密集爆发。印象最深的一次故障用户问“我的订单到哪里了”模型先调用了“查用户信息”接口参数传了一个字符串“帮我查一下”。后端当然不认返回参数错误。模型收到错误后没有重试没有换接口而是开始道歉甚至编造了一段物流信息。这个场景相当典型——不是模型不会调用工具而是工具列表太扁平、参数约束缺失、失败后没有兜底策略导致模型在真实输入面前完全裸奔。那段时间我意识到光有一堆 function 远远不够。我们需要的是agent-skills一套把能力、约束、执行策略、失败处理都封装好的“技能”体系让模型以最低的认知成本知道“何时用、怎么用、用完怎么处理”。1.2 Skill 与 Tool一字之差差在“边界”很多人把 Skill 和 Tool 混为一谈这是后续所有混乱的根源。Tool 只是一个可被调用的函数而 Skill 是包含完整“使用说明”的最小业务单元。我用一个类比来解释Tool 像一颗螺丝钉你把它扔给模型模型知道这玩意儿能拧东西但不知道什么时候该拧、拧几圈、拧歪了怎么办。Skill 像一个带有标准接口的模块化组件上面贴着铭牌用途、适用条件、输入规格、输出格式、异常代码。模型只需要按照铭牌操作不需要在使用时重新推理这个工具到底该怎么用。从工程实现看二者的差异更具体。Tool 层面的调用是“模型自由发挥”模型决定调用哪个函数、传什么参数系统的干预极少。Skill 层面的调用则多了一层强约束技能描述规范了触发条件JSON Schema 规范了参数格式执行器封装了内部逻辑错误处理规定了失败路径。这层强约束不是限制模型而是保护模型——尤其在参数复杂、执行链路长、失败代价高的场景里约束越清晰模型的表现反而越稳定。我重构执行层时定了一条原则任何被模型直接调用的能力都必须以技能形式注册不允许再出现“裸函数”。这条原则执行下来线上问题减少了大概六成。2. 技能系统的骨架注册、声明、编排三层各管什么2.1 注册层让模型知道 agent 手里有哪些牌技能系统的第一层是注册层。它的职责很简单维护一份当前 agent 可用技能的完整清单这份清单既给模型看也给运行时用。给模型看意味着技能清单要作为上下文的一部分注入到模型输入中。这里有一个容易忽略的点技能说明属于“系统级信息”不是对话信息。我在早期版本里把技能说明拼在用户消息后面结果模型经常把它们当成对话历史干扰了正常的语义理解。正确的做法是放在系统提示词中与对话内容明确隔离。更合理的方案是走 function calling 的声明通道让平台把技能定义结构化地传给模型而不是靠自然语言描述。给运行时用意味着注册表还要记录每个技能的元数据名称、版本、参数 Schema、超时时间、执行模式、关联的错误码。这些信息在技能调用前需要被校验和检索。我最终用一张注册表结构来管理字段说明示例name技能唯一标识动词开头小写加下划线query_order_statusdescription面向模型的技能说明根据订单号查询订单当前物流状态parametersJSON Schema 参数定义见下文示例timeout技能执行超时时间10smode执行模式sync / asyncsyncversion技能版本号用于灰度与回滚1.2.0命名规范值得多说两句。技能名称最好用“动词_对象”的结构比如query_customer_info、calculate_delivery_time不要出现do_stuff、helper这类语义不明的名字。模型对名称的语义理解直接影响它是否会选用这个技能命名不清会导致技能被雪藏或者被误用。2.2 声明层技能描述的写法直接决定成功率注册层解决“有哪些技能”声明层解决“模型到底能不能正确使用”。这一层是技能系统的灵魂也是大多数人做得最粗糙的地方。先说技能描述description怎么写。我见过很多例子写的是“查询订单”四个字就完了。这种描述丢给模型模型只能猜。正确的写法至少要覆盖四件事触发条件、能力范围、限制条件、典型示例。这是我改造过的一个描述根据订单号查询订单的当前物流状态与预计送达时间。当用户咨询“我的订单到哪了”“快递什么时候到”“发货了没”等问题时使用本技能。仅支持查询近 90 天内的订单订单号格式为 19 位数字。示例输入{order_id: 2025080112345678901}这段描述里有明确的触发条件用户问什么时用有边界约束仅支持 90 天内有格式要求19 位数字有示例。模型看到之后触发意图判断和参数填写的准确率会明显提升。再说参数 Schema。如果你把参数定义成宽松的 object模型就会往里塞各种奇奇怪怪的东西。我要求所有参数必须显式声明类型、必填性、描述可枚举的字段必须列 enum。下面是一个参数 Schema 的正面例子{ type: object, properties: { order_id: { type: string, description: 19位数字订单号 }, query_type: { type: string, enum: [status, express], description: 查询类型status 查询物流状态express 查询快递公司 } }, required: [order_id, query_type] }有 enum 就别让模型自由发挥。模型是概率推理器给它越多的“答案选项”它选对的概率越高留白越多它越容易自己创造答案。2.3 编排层把技能串成流程而不是让模型自由发挥当任务链路变长比如“查订单 - 算配送时间 - 生成回复”让模型在每一步都自由选择技能效果往往不可控。模型可能在第一步就跳到第三步也可能在中间插入无关技能。我的做法是加入一层编排层把高频、稳定的流程预先定义成模板模型只需要做“填空”和“分支判断”。编排层支持三类模式串行模式前一个技能的输出作为后一个技能的输入适合固定流水线。比如“解析收货地址 - 调用地址标准化技能 - 计算运费”。并行模式多个技能之间无依赖同时触发适合需要聚合多源信息的场景。比如“查询商品信息”和“查询库存”同时执行最后统一汇总。条件选择模式根据中间结果动态决定下一步。比如订单状态是“已签收”就走售后技能是“运输中”就走催单技能。条件分支可以用一个轻量级的规则引擎表达也可以用简单的 if-else 代码实现关键在于把决策点显式化而不是放任模型每次自己拍脑袋。引入编排层之后模型从“自由调用者”变成了“任务执行者”这实际上是把一些确定性逻辑从概率推理中剥离出来整个系统会更稳。3. 一套可直接参考的 agent-skills 实现3.1 技能模块的数据结构设计聊完设计原则直接看代码。我用 Python 实现了这套技能系统的核心骨架。首先是技能的数据结构from dataclasses import dataclass, field from typing import Any, Callable, Dict, Optional dataclass class SkillSpec: name: str description: str parameters: Dict[str, Any] timeout: float 10.0 version: str 1.0.0 handler: Optional[Callable[..., Any]] None dataclass class SkillResult: skill_name: str success: bool data: Any None error_code: str error_message: str raw_output: str class SkillRegistry: def __init__(self): self._skills: Dict[str, SkillSpec] {} def register(self, spec: SkillSpec): if spec.name in self._skills: raise ValueError(fskill {spec.name} already registered) self._skills[spec.name] spec def get(self, name: str) - Optional[SkillSpec]: return self._skills.get(name) def list_skills(self) - list: return [{name: s.name, description: s.description} for s in self._skills.values()]SkillSpec有两点值得注意。一是把parameters直接设计成 JSON Schema 的 dict这样后续既可以序列化传给模型做 function calling 声明又可以用 jsonschema 库在本地做参数校验一份定义两处使用。二是timeout和version是必填项避免上线的技能没有超时保护、没法灰度回滚。3.2 写一个带参数校验的技能示例定义好结构之后写一个具体技能。这里以订单查询为例import jsonschema from jsonschema import ValidationError def build_query_order_skill() - SkillSpec: schema { type: object, properties: { order_id: { type: string, description: 19位数字订单号 }, query_type: { type: string, enum: [status, express], description: 查询类型 } }, required: [order_id, query_type] } def handler(order_id: str, query_type: str) - dict: # 这里替换为真实后端调用 if not order_id.isdigit() or len(order_id) ! 19: raise ValueError(invalid order_id format) if query_type status: return {status: shipping, location: 杭州转运中心} return {express: 顺丰速运, tracking_no: order_id} def execute(*, order_id: str None, query_type: str None) - SkillResult: payload {order_id: order_id, query_type: query_type} try: jsonschema.validate(payload, schema) data handler(order_idorder_id, query_typequery_type) return SkillResult(skill_namequery_order_status, successTrue, datadata) except ValidationError as e: return SkillResult( skill_namequery_order_status, successFalse, error_codeINVALID_PARAMS, error_messagestr(e) ) except Exception as e: return SkillResult( skill_namequery_order_status, successFalse, error_codeEXEC_FAILED, error_messagestr(e) ) spec SkillSpec( namequery_order_status, description根据订单号查询订单当前物流状态或快递公司。用户在咨询物流信息时使用。, parametersschema, timeout8.0, version1.0.0, handlerexecute ) return spec这个实现里有一个关键设计handler是内部业务函数execute才是暴露给调度器的入口。execute里面做两层处理第一层用 jsonschema 做参数预校验第二层把业务异常统一转换成SkillResult的结构化错误码。为什么要这样因为模型调技能时传参的错误五花八门如果不做预校验错误信息会非常难看而且会把业务层的堆栈信息直接暴露给模型既误导模型又增加上下文噪音。3.3 运行时调度从模型输出到技能执行的完整链路调度器是技能系统的心脏。它负责接收模型的工具调用请求解析参数匹配技能执行并返回结果。以一个简化的循环为例import json import time class SkillScheduler: def __init__(self, registry: SkillRegistry): self.registry registry def invoke(self, skill_name: str, arguments: str) - SkillResult: spec self.registry.get(skill_name) if spec is None: return SkillResult( skill_nameskill_name, successFalse, error_codeSKILL_NOT_FOUND, error_messagefskill {skill_name} is not registered ) # 解析模型返回的 arguments它通常是一个 JSON 字符串 try: params json.loads(arguments) if isinstance(arguments, str) else arguments except json.JSONDecodeError as e: return SkillResult( skill_nameskill_name, successFalse, error_codeINVALID_PARAMS, error_messagefarguments is not valid json: {e} ) try: start time.time() result spec.handler(**params) result.execution_ms int((time.time() - start) * 1000) return result except Exception as e: return SkillResult( skill_nameskill_name, successFalse, error_codeUNEXPECTED_ERROR, error_messagestr(e) )完整的大模型调用循环不会再展开到生产级代码核心流程是把用户问题连同技能清单发给模型 - 模型决定调用某个技能并生成 arguments - 调度器解析、校验、执行 - 把结构化执行结果返回给模型 - 模型基于结果继续推理或结束。这个过程里有一个看着不起眼但极其重要的细节从模型返回的 arguments 是一个 JSON 字符串直接json.loads存在失败可能。模型偶尔会生成残缺的 JSON比如少一个引号。我在生产环境加了一道自动修复逻辑——捕获 JSONDecodeError 后尝试用正则提取所有 key-value实在修复不了再抛INVALID_PARAMS。这个兜底让调度层的报错率下降了不少。4. 我在真实业务里踩过的三个坑定位过程与修复方案4.1 坑一模型把字符串参数填成了 JSON 对象某个技能需要接收一个customer_id字符串结果模型在 arguments 里传了{customer_id: {id: 12345}}把字符串填成了对象。我当时的第一反应是“模型傻了”但把原始输出打出来仔细分析之后发现问题出在参数 Schema 本身的描述有歧义——我在customer_id的描述里写了“从用户信息中提取 ID”模型误以为要填入整个用户信息对象。定位过程走了一条完整的链路先在调度层打印每次调用的模型原始 arguments发现异常数据然后对照 Schema 检查描述最后确认是描述语义让模型产生了错误联想。修复方案有两步一是把参数描述改成更明确的“仅填入 19 位数字 ID不要包含其他信息”二是给代码层加了一道运行时后校验——如果customer_id不是字符串禁止进入业务层直接返回INVALID_PARAMS。经验是不要指望模型天然理解你的参数格式描述里每多一分歧义线上就多一分出错概率。同时校验逻辑坚决不能省模型输出不可信这是所有 agent 工程的铁律。4.2 坑二超长执行结果把上下文窗口“腌入味”了另一个问题出现在技能返回结果的消费方式上。某个分析类技能会返回一条很长的结构化数据包括几十条明细记录。起初我把完整结果直接拼到上下文里回传给模型结果发现token 消耗陡增模型后续生成的回复质量反而下降甚至开始“复读”明细数据里的噪声内容。定位过程比较直接监控大模型接口的 token 用量曲线发现某个技能被调用后后续轮次的输入 token 突增且持续不降。原因很明显——原始结果里的大部分字段对模型后续推理是没有价值的它们只是业务细节却占据了上下文窗口稀释了真正的关键信息。我的修复是在调度器和模型之间加了一层“结果精炼器”。每个技能可以声明自己的summarizer函数对原始结果做裁剪、聚合、摘要。比如明细列表只保留前三条加“等共 N 条”状态码映射成人类可读的描述原始日志落到日志系统但绝不进上下文。同时给SkillResult增加raw_output和model_output两个字段前者进日志后者进模型上下文彻底隔离。这一改动之后单次技能调用的 token 消耗平均降了 40% 左右模型回复的稳定度也上了一个台阶。上下文不是垃圾场不要什么东西都往里倒。给模型看什么不给模型看什么是技能系统的重要控制面。4.3 坑三两个技能共用一个临时目录数据互相覆盖这是最隐蔽的一个坑。业务侧上线了一个“报表导出”技能同时老系统里有一套“批量导入”技能两者内部都会向同一个/tmp/report_data目录写临时文件。单独测试都正常一旦两个技能在相近时间被触发就会出现一方导出的数据被另一方覆盖用户收到损坏文件。排查过程花了不少时间因为错误是间歇性的且复现条件苛刻。最终还是靠链路追踪定位把每个技能的执行 ID 关联到文件写入路径发现两个技能写入了同一个文件名。根因不复杂但在技能数量变多之后全局名称空间的冲突问题一定会暴露。修复方案是给每个技能调用分配独立的沙箱工作目录目录名带上技能名和请求 ID比如/tmp/skills/export_report/req_8f3a2c/执行结束后统一清理。更进一步我还给所有外部资源命名加上了全局前缀包括 Redis key、消息队列 topic、临时表名。一套规范下来交叉污染问题基本绝迹。这个坑提醒我当 agent 的技能数量超过一定规模它们就不再是孤立的函数而是一个分布式系统的多个工作负载。资源隔离必须从设计层面解决不能在出了问题之后靠“下次注意”补救。5. 给技能系统加“元能力”自省、组合与优雅降级5.1 技能自省让 agent 知道自己几斤几两技能数量少的时候把全部技能清单一次性注入上下文即可。但当我注册的技能超过 20 个之后一次性注入会占用大量 token而且模型面对过多选择时反而会“选择困难”频繁选错技能。我引入了一个list_skills元技能。它的作用不是查询数据库而是返回当前已注册技能的轻量级目录——只有名称和一句话描述。模型先调用list_skills了解全貌再根据任务需要决定是否需要进一步查看某个技能的详细定义。这对多层级的技能架构特别有用顶层是目录二级是详细技能定义模型按需加载。自省还有一个隐藏收益它让 agent 具备了“知道自己知道什么”的能力比“默默尝试”更能避免误导用户。当用户问了一个完全不在技能范围内的问题模型可以基于目录做出“不在服务范围内”的判断而不会硬着头皮瞎编。5.2 组合技能把原子步骤编排成模板原子技能提供单一能力组合技能把多个原子技能按固定流程编排起来形成更复杂的业务能力。组合技能的实现可以很轻量在技能内部再调用调度器。举个例子“订单全链路分析”技能实际上由三个子技能串联而成query_order_status、calculate_delivery_time、estimate_arrival_window。组合技能的 handler 里按顺序调用这些子技能任何一步失败都返回结构化的错误码和失败点而不是把半截数据交给模型。def combined_handler(order_id: str) - SkillResult: # 这里使用同一个 scheduler 调用其他技能 r1 scheduler.invoke(query_order_status, json.dumps({order_id: order_id, query_type: status})) if not r1.success: return SkillResult(skill_nameanalyze_order_fullchain, successFalse, error_codeSUB_SKILL_FAILED, error_messagequery_order_status failed) r2 scheduler.invoke(calculate_delivery_time, json.dumps({order_id: order_id})) if not r2.success: return SkillResult(skill_nameanalyze_order_fullchain, successFalse, error_codeSUB_SKILL_FAILED, error_messagecalculate_delivery_time failed) return SkillResult(skill_nameanalyze_order_fullchain, successTrue, data{status: r1.data, delivery: r2.data})组合技能的好处是对模型暴露的接口数量更少每个接口的语义更稳复杂流程的逻辑被固化在代码里而不是每次靠模型自由编排。这本质上是在 agent 体系里画了一道清晰的线——线以上是模糊的推理线以下是确定的逻辑。5.3 降级策略失败之后怎么办比失败本身更重要技能执行失败不可怕可怕的是失败之后模型陷入“胡言乱语”。我在设计错误码体系时定了一套约定每个技能的错误码只能取以下几种错误码含义模型应该怎么办SKILL_NOT_FOUND技能不存在告知用户能力不足不要尝试替代INVALID_PARAMS参数不合法查看技能定义修正参数后重试EXEC_FAILED业务逻辑异常判断是否可以换一种方式处理TIMEOUT执行超时告知用户系统繁忙请稍后再试UNEXPECTED_ERROR未知错误记录日志统一兜底话术降级策略的核心是每一种错误码都对应模型的一种“行为模式”而不是让模型自由发挥。比如INVALID_PARAMS要触发“修正参数重试”TIMEOUT要触发“暂停重试并告知用户”。我在系统提示词里明确约束了这些对应关系模型按错误码执行行为策略而不是读了错误信息之后自己猜该怎么办。实测下来加了降级策略之后最明显的变化是“失败后的对话质量”稳定了。用户至少能得到一个明确的答复而不是一段毫无帮助的道歉。6. 技能系统的测试与验收覆盖率和回归用例6.1 给每个技能建一张“用例卡”开发阶段我踩过一个大坑技能本身写得没问题但对“模型会怎么调用它”缺少验证。后来我养成了一个习惯——为每个技能建一张用例卡至少包含三类用例正常路径用例模拟用户真实意图验证模型能否在给定对话中选中该技能、参数是否填对、结果是否正确返回。边界参数用例空字符串、超长字符串、缺失必填字段、非法枚举值验证调度层能否给出结构化校验错误。错误路径用例后端接口超时、返回非预期格式、技能内部抛异常验证错误码转换和模型后续行为。这些用例不是跑一遍就完了而是沉淀成离线回归集。每次技能描述有改动、参数 Schema 有调整、模型版本有升级都要重新跑一遍。我有一次只改动了query_type的枚举值结果发现模型的参数填充准确率掉了 12 个百分点靠回归用例抓了出来。技能系统的脆弱性往往藏在描述的一词之差里回归测试是最低成本的防线。6.2 成本与延迟的权衡最后聊一个运维向的观察。技能系统不是越复杂越好。每多一层封装都会带来额外的延迟和 token 成本。技能注册表查询、参数校验、结果精炼、错误码转换这些环节单看都不重但叠加在一次大模型调用链路上可能增加几百毫秒甚至数秒。我的权衡原则是固定流程和强约束逻辑尽量下沉到编排层执行模型只负责理解和决策能在一个技能内部完成的事不要拆成多个技能让模型去串联经常一起被调用的原子技能优先考虑合并成组合技能。技能系统的设计本质上是在模型的“自由度”和系统的“确定性”之间取一个平衡点而这个平衡点没有标准答案只能在自己的业务数据里反复测量。从最初三个接口的裸调用到如今几十个技能的系统化管理这个重构过程持续了大半年。我最大的体会是agent 应用的上限确实取决于模型能力但它的下限完全由工程决定。技能描述写得糊弄模型再聪明也会出错调度链路不设防线上问题就会反复出现。技能系统不是什么新奇的技术它是对“模型不可控”这件事的工程化妥协——承认模型会犯错然后用结构、约束和兜底把错误的代价降到最低。如果要分享一个最值得回头看的小技巧那就是把每次生产环境失败的模型原始输出都保存下来定期导成离线用例回放到新版本的系统里。这个习惯救了我很多次因为模型的行为会随版本变化而技能系统真正的稳定性都是在这些真实错误的反复打磨中练出来的。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号