Agent技能系统实战:从零搭建标准化技能单元提升多步任务自动化

发布时间:2026/10/13 5:51:21
Agent技能系统实战:从零搭建标准化技能单元提升多步任务自动化 在折腾了大半年Agent相关的东西之后我越来越笃定一个观点Agent好不好用拼的不是模型有多聪明而是你给它配了什么样的技能系统。这里的“技能”不是说让模型学会某个动作而是指一套能被模型识别、触发、执行、校验的标准化能力单元。社区里管这套东西叫agent-skills本质上是把模型之外的工具、流程、知识通通打包成可复用的模块让Agent在遇到具体任务时能想得起来、拿得出手、跑得完。这篇文章我就围绕agent-skills把自己从零搭建这套系统的过程、踩过的坑和改进思路完完整整写一遍适合正在做Agent应用开发、或者研究多步任务自动化的开发者参考。先说清楚它能解决什么问题。单纯用Prompt塞指令Agent面对两三个步骤以内的简单任务还行一旦任务链路变长——比如“读取配置、解析日志、定位异常、生成报告”——模型就开始发挥想象力步骤跳来跳去执行结果经常是“看起来对”但根本跑不通。技能系统的作用就是把这种不确定性收拢到一个个明确、可测试的单元里让模型只做“决定调用哪个技能”这件事而不是让它自己去猜测怎么操作文件、怎么调接口、怎么拼SQL。文章里没有故弄玄虚的架构图和框架包装只有我实际写过的代码、实际翻车的场景和后续的修补方案。如果你也在做Agent应用尤其是要让Agent处理真实业务数据而不是陪聊这篇文章应该能帮你省掉不少试错时间。1. 为什么给Agent加一套技能系统1.1 从低成功率翻车说起我最初做Agent的方式很朴素写一段很长的System Prompt把所有工具说明、使用限制、输出格式全部塞进去然后让模型自己决定怎么干。在Demo阶段确实能跑通模型会照着说明调用工具函数。但放到真实场景里问题立刻暴露。第一是描述膨胀。工具一多Prompt里光工具说明就占了小一万字模型的处理能力被大量无关信息稀释。我试过同时暴露二十多个工具模型在二选一时经常选错甚至把两个不相关的工具参数交叉使用。第二是上下文污染。每次工具调用返回的完整结果直接塞回对话下一次调用时模型要在一片混乱的输出里找关键信息准确性肉眼可见地往下掉。第三是维护地狱。工具函数和Prompt描述是两份东西改一个函数忘记改描述模型就会照着旧描述调用参数对不上异常处理逻辑只能靠外部兜底。这些问题的根源都一样能力没有结构化。工具、描述、参数、执行逻辑全部混在“文本函数”的松散组合里模型只能靠语义硬猜。某开发者跟我交流时打了个比方说这就像让一个新人同时记住一百条工作流程还不给他看制度手册全靠悟性。技能系统的本质就是给Agent一份“制度手册”每条技能都有清晰的触发条件、输入标准、输出格式和校验规则。1.2 技能系统到底在解决哪些具体问题把技能系统拆开看它真正解决的是四件事。第一件事是上下文压缩。模型不需要看到工具的全部实现代码和冗长文档只需要看到一份简短的技能描述。我在实测中把二十多个工具的说明压到了三千字以内每项技能平均一百字左右模型的选择准确率反而提升了。第二件事是能力复用。同一个技能可以被不同任务调用也可以被编排进不同的流程。比如“读取文件”这个技能既能在日志分析任务里用也能在代码审查任务里用还能在配置检查任务里用。不需要为每个任务重写一遍文件读取逻辑。第三件事是确定性回归。每个技能都是独立的函数可以单独测试、单独加日志、单独做异常处理。模型调用出错时你能立刻看出是模型选错了技能还是技能本身执行失败而不是在一个黑盒里猜。第四件事是扩展性。新增加一个能力只需要注册一份新技能移除一个能力只需要在注册中心摘掉它。Agent的主流程不用动Prompt也不用动整个系统可以像插积木一样持续演进。2. 技能系统的核心概念与架构选型2.1 技能的最小定义一份能说清楚的说明书我最早踩的坑是把技能定义得太“函数化”光是函数签名和参数类型模型根本看不懂什么时候该用。后来我逐步摸索一份合格的技能说明书至少要有六个部分技能名称、一句话描述、触发场景、参数JSON Schema、执行函数、示例对。技能名称要短且有区分度。不要用read_file_util_2024这种名字就用read_file模型一眼能看出用途。描述是给模型看的要写清楚“什么时候用、输入是什么、输出是什么”。我见过很多描述写成了功能说明书比如“读取文件内容并返回字符串”这等于没写。真正有用的描述是“当用户需要查看或获取磁盘上某个文件的内容时使用传入路径返回文件全部内容”。触发场景一定要具体抽象描述会让模型宁可猜也不调用。参数用JSON Schema定义这是技能系统里最容易被忽略的一环。模型需要明确的参数结构包括每个参数的类型、是否必填、取值范围。我试过让模型自由传参结果它把路径参数塞进了超时参数里。JSON Schema能极大降低这种错误因为模型在推理时会按结构去组织参数。示例对是给模型做few-shot参考的一个“成功调用示例”加上一个“失败调用示例”模型对技能边界的理解会清晰很多。2.2 注册中心、调度器、执行器三个核心组件各管什么一个最小可用的技能系统至少要有三个组件配合。注册中心负责维护技能清单。它是一个字典结构键是技能名值是对应的技能对象。技能启动时通过装饰器或手动注册的方式登记进去运行中还能动态增删。调度器负责接收模型的调用指令解析出技能名和参数然后分发到具体技能。它还要处理“技能不存在”“参数校验失败”“技能执行超时”这些异常分支把错误原样返回给模型让模型有机会修正调用。执行器是真正干活的部分也就是技能背后的普通函数。它不应该感知模型的存在输入是参数输出是结构化结果。我在设计执行器时加了一层统一封装把返回值统一包装成“状态数据”的结构调度器只看状态就能判断成功还是失败不用关心具体业务逻辑。表格三个组件的职责边界组件核心职责错误处理方向注册中心技能登记、查询、动态增删技能不存在时返回明确错误码调度器解析调用指令、分发参数、控制重试参数异常时返回可读错误信息执行器执行具体逻辑、返回结构化结果业务异常时包装成状态码返回这套结构的好处是边界清晰。模型只与调度器打交道调度器只与注册中心打交道注册中心只与技能对象打交道。任何一层出问题都能快速定位不会互相牵扯。3. 从零实现一个Mini技能系统3.1 技能定义与注册的落地代码我直接展示一个可以跑的示例代码这个代码我是在一个某文件管理Demo项目里反复迭代出来的核心逻辑非常简洁。from dataclasses import dataclass, field from typing import Any, Callable, Optional dataclass class Skill: name: str description: str parameters: dict handler: Callable[..., Any] examples: list field(default_factorylist) tags: list field(default_factorylist) SKILL_REGISTRY: dict[str, Skill] {} def register_skill( name: str, description: str, parameters: dict, handler: Callable[..., Any], examples: Optional[list] None, tags: Optional[list] None, ): skill Skill( namename, descriptiondescription, parametersparameters, handlerhandler, examplesexamples or [], tagstags or [], ) SKILL_REGISTRY[name] skill return skill注册中心就是一个字典没有复杂设计。真正需要注意的是参数校验。模型传给调度器的参数经常是字符串类型和JSON Schema里声明的int类型不一致所以我在注册中心之外加了一个参数清洗器把能强转的类型统一转一遍转不了的报错返回。def clean_params(schema: dict, raw: dict) - dict: cleaned {} errors [] for prop_name, prop_schema in schema.get(properties, {}).items(): if prop_name not in raw: if prop_name in schema.get(required, []): errors.append(fmissing required param: {prop_name}) continue value raw[prop_name] expected_type prop_schema.get(type) if expected_type integer: try: value int(value) except (ValueError, TypeError): errors.append(fparam {prop_name} should be integer) elif expected_type string: value str(value) elif expected_type array: if not isinstance(value, list): errors.append(fparam {prop_name} should be array) cleaned[prop_name] value if errors: raise ValueError(; .join(errors)) return cleaned这段代码不复杂但它解决了一个真实问题模型输出的参数值常常带着引号、逗号或者多余空格不清洗就直接执行函数轻则拿到错误结果重则执行器内部直接抛类型异常。清洗器把所有参数统一校正后再交给处理函数异常率明显下降。3.2 调度器让Agent按JSON协议调用技能模型怎么告诉系统它想调用哪个技能我用的方案是让模型输出一段JSON格式固定为两个字段技能名和参数。这种结构化输出比让模型直接执行代码安全得多也容易做日志审计。def dispatch(raw_call: str) - dict: import json try: call json.loads(raw_call) except json.JSONDecodeError as e: return {status: parse_error, message: finvalid JSON call: {e}} skill_name call.get(skill) params call.get(params, {}) skill SKILL_REGISTRY.get(skill_name) if skill is None: return {status: skill_not_found, message: funknown skill: {skill_name}} try: cleaned clean_params(skill.parameters, params) except ValueError as e: return {status: param_error, message: str(e)} try: result skill.handler(**cleaned) return {status: success, skill: skill_name, result: result} except Exception as e: return {status: exec_error, message: str(e)}调度器返回的状态码我一开始只用了success和error两种后来发现排查问题太费劲。加了skill_not_found、param_error、exec_error三种细分状态后定位问题的时间至少缩短了一大半。日志里扫一眼状态码就知道问题出在调度环节还是执行环节。这里还要提醒一个关键点模型调用技能后调度器返回的结果会重新进入模型上下文所以返回内容千万不能太大。如果技能返回一个很大的列表模型下一轮推理会被无关信息带跑偏。我会在技能执行结果外套一层摘要比如返回文件时只返回前几行和行数完整内容写到临时文件把文件路径作为结果的一部分返回。这样上下文里永远只保留“小而关键”的信息。3.3 一个可直接用的文件管理技能包理论讲太多容易飘我直接给一个可以抄作业的文件管理技能包。它包含三个技能读文件、写文件、目录搜索。这三个技能在自动化任务里使用频率非常高而且踩坑案例典型。import os from pathlib import Path def read_file(path: str, max_lines: int 50, tail: bool False): p Path(path) if not p.exists(): return {error: ffile not found: {path}} if p.is_dir(): return {error: fpath is a directory: {path}} with open(p, r, encodingutf-8, errorsreplace) as f: lines f.readlines() total len(lines) if tail: lines lines[-max_lines:] else: lines lines[:max_lines] return {total_lines: total, showing: len(lines), content: .join(lines)}读文件这个技能我的经验是永远不要无限制全量读。大文件读进来模型上下文直接爆炸。默认最多返回50行完整信息通过额外参数控制这算是一种防御式设计。写文件的技能也需要做边界控制。我规定只能写项目根目录下的文件路径必须先从相对路径转绝对路径再用Path.resolve校验前缀防止越权写系统目录。这个保护不能省因为模型一旦被注入恶意指令写文件技能就是最大的破坏入口。BASE_DIR Path(__file__).resolve().parent def write_file(path: str, content: str): p (BASE_DIR / path).resolve() if not str(p).startswith(str(BASE_DIR)): return {error: path outside allowed base dir} p.parent.mkdir(parentsTrue, exist_okTrue) p.write_text(content, encodingutf-8) return {written: str(p), length: len(content)}目录搜索技能实现也不复杂但注意要限制搜索深度和最大返回条数防止模型在超大目录树下递归到卡死。三个技能注册后我在测试里让模型执行“在项目里找一个包含error关键字的日志文件然后复制到output目录”。模型先调用目录搜索返回候选文件列表再调用读文件查看内容最后用写文件完成复制。整个流程不需要任何硬编码脚本完全靠技能系统跑通。这就是agent-skills的价值模型只负责决策每个决策都落在一个确定性执行单元上。4. 常见问题与排查技巧实录4.1 Agent就是不调用技能怎么救这个问题出现频率极高。技能注册得很完整函数都能单测通过但模型在对话里就是不用非要自己硬编一段答案。我排查这类问题的经验顺序是固定的。先看技能描述是否足够具体。描述里如果只写“读取文件”模型不知道什么时候该用。我改成“当用户需要查看文件内容、检查日志、读配置项时使用”触发场景清晰后调用率明显上升。再看参数结构是否简单。参数越多、越复杂模型越倾向于回避。能合并的参数尽量合并能设置默认值的尽量设置默认值。然后看是否有示例。一个技能哪怕描述写得好模型初次见到还是容易犹豫。我给每个技能配一组示例后调用率提升了三成以上。示例要让模型直接模仿所以格式要和真实使用的输入输出完全一致。最后检查技能列表长度。单轮对话暴露给模型的技能不要超过十五个超过后选择准确率会断崖式下跌。如果技能很多就采用分组策略先让模型选“能力域”再在里面选具体技能。4.2 执行结果浪费上下文怎么压缩技能执行成功后返回值如果直接全部进入上下文相当于把一段外部世界的数据原样塞进了模型的短期记忆。短期记忆容量有限塞满之后模型就会“忘记”最初的任务目标。我的解法是分级结果摘要。第一层技能内部做裁剪比如列表只返回前十条第二层调度器再做包装将结果转成简短的JSON结构删除所有不影响语义的字段第三层在Prompt层面告诉模型“技能结果只看关键字段不要重复全量内容”。三层下来上下文占用能减少百分之六七十任务完成率和稳定性都会提升。另一个技巧是把大块内容放到外部存储技能只返回一个引用ID。模型需要时再通过引用技能按需取回。这种“浅引用”模式非常适用于处理代码库、文档集、日志文件这些数据密集型场景。4.3 技能太多之后调度准确性怎么保证技能数量从几个增长到几十个之后即使每个技能的描述都写得很好模型的选择也会开始飘。我试过在一个Demo里注册四十多个技能模型的选择准确率下降到惨不忍睹。解决办法是给技能分组引入两层调度。第一层只向模型暴露“能力域”比如文件操作、数据处理、网络请求、流程控制每个域下一组技能。模型先选域选完后再展示域内技能。这样每次的候选技能数量都控制在合理范围内准确率锐减问题基本消失。我还会给技能加权重和别名。常见的技能权重调高命中的优先排序容易混淆的技能加上别名提示让模型在看到描述时更容易做区分。比如“search_files”和“find_in_files”容易混淆我把后者改名为“grep_in_files”同时别名字段写明“如果是查找文件名称用search_files如果是查找文件内容用grep_in_files”模型就不容易搞错了。表格技能调度准确率优化的关键调整调整手段适用场景实测效果精简单轮暴露数量技能总数30个以上准确率从不足六成恢复到八成左右分组两级调度技能跨多个能力域单轮决策压力显著降低示例对补充新技能首次接入时调用率提升约三成重名和别名校验功能相似的技能并存时混淆调用基本消失5. 进阶玩法技能编排与让Agent自己写技能5.1 把多个技能串成Pipeline当技能数量到一定规模我开始不满足于“让模型随机应变地调用”而是希望它能按固定流程执行重复性任务。这就引入技能编排也就是Skill Pipeline的概念。最简单的编排方式是顺序执行技能A的返回结果作为技能B的输入参数。我实现了一个轻量的编排器用规则文件描述流程模型只负责填充流程中的参数不负责跳步。比如“日志分析”流程固定是先搜索文件再解析内容最后生成摘要。{ pipeline: log_analyze, steps: [ { skill: search_files, output: matched_files }, { skill: read_file, input_from: matched_files, output: file_content }, { skill: summarize_log, input_from: file_content, output: report } ] }编排的好处是确定性大幅提升。任务哪怕涉及十几个技能每一步都执行在可控的轨道上不会出现模型天马行空的跳变。坏处是灵活性降低所以我只对高频固定流程做编排其他场景仍然让模型自由调度。实测下来固定流程用编排探索性任务用自由调度整体效果最好。5.2 让Agent自己写技能进阶玩法里最让我兴奋的部分是让Agent具备“自定义新技能”的能力。它不是人类预先写好的而是Agent在运行过程中通过分析用户需求动态生成并注册一个新技能。实现思路是给Agent暴露一个“创建技能”的特殊技能。它的执行逻辑是接收一个符合规范格式的Python函数源代码和描述将源码保存到技能目录然后在注册中心动态加载并注册。整个过程加上一层校验新技能必须通过一个非法参数测试和一个边界输入测试校验不通过就自动回滚。这个功能在真实场景中非常实用。比如某次用户想让Agent生成一个特定格式的报表内置技能没有覆盖。Agent可以先分析报表格式生成一段处理函数注册成新技能之后再次遇到同类任务就能直接调用不再需要从头推理。这相当于Agent拥有了可持续进化的能力。但要注意动态生成代码的风险必须严格管控。我的经验是四条底线不能破只能在沙箱目录里创建文件不允许访问网络技能源码必须经过本地AST静态检查禁止导入危险库注册前必须跑一遍单元测试所有自动生成的技能都打上“auto_generated”标签方便后续审计。我在实际使用中发现让Agent自动写技能不能一上来就放开权限。最早我让它注册技能它直接把一段有文件删除逻辑的代码注册成功了还好沙箱目录隔离了真实文件系统不然就出大问题。从那之后我坚持所有自动注册技能必须经过校验流程宁可让创建失败多一点也不能让不可控代码混入技能库。跨界扩展一下这个机制用到业务报表自动化上效果很夸张。有一次我让Agent分析一份销售数据它发现需要“按地区聚合后计算环比变化”的能力然后自动生成并注册了这个技能。之后所有同类报表需求它都直接调用新技能完成速度和准确性比之前靠Prompt临时推理好太多。让Agent具备可持续进化的技能库是我做这套系统以来觉得最大值的投入方向。