ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

agent-skills实战:将大模型Agent能力模块化,提升可控性与扩展性

agent-skills实战:将大模型Agent能力模块化,提升可控性与扩展性 1. 先说清楚agent-skills 到底在解决什么问题这两年做大模型应用的人应该都有同感单轮对话早就卷到头了真正拉开差距的是 Agent——能让模型自己决定“下一步做什么”的那种应用。但很多人把 Agent 做成一个大而全的 prompt里面塞满了工具列表、使用规则、边界条件结果模型要么记不住、要么用错、要么干脆乱来。我自己也踩过这个坑直到后来认真研究了 agent-skills 这套思路才算是把 Agent 的可控性和扩展性同时提了上来。简单说agent-skills 的核心思路是把 Agent 能执行的每一项能力拆成独立、可描述、可校验的“技能模块”。每个技能模块包含清晰的名称、用途说明、输入参数定义、执行逻辑和返回格式。Agent 在运行时根据用户请求动态选择技能、调用技能、解析结果而不是靠一段臃肿的 prompt 硬撑。这就像把一个大工具箱里的每件工具都贴上标签、写明用途和注意事项而不是把所有工具倒在一张桌子上让新手自己猜。这套方案特别适合三类人一是正在做客服、 Copilot、智能助手类产品的同学需要处理大量重复但变化多端的用户请求二是在做企业内部知识库问答、数据查询、工单自动化这类“必须保证输出准确”的场景三是刚开始接触 Agent 开发、被各种框架绕得晕头转向的新手——先把技能体系想明白后面选什么框架都是顺手的事。2. 技能化拆解为什么要把 Agent 能力“模块化”2.1 从“大而全的 prompt”到“小而专的技能库”我第一次做 Agent 时习惯把所有能力写进一个系统提示词里。比如同时让模型能做天气查询、订机票、查汇率、算房贷……功能看着很全但实际跑起来问题一堆模型经常混淆不同任务的参数格式用户说的话稍微拐个弯模型就不知道该调用哪个工具每次想加一个新功能都要小心翼翼地改那段已经很长的 prompt生怕把之前调好的行为搞坏。agent-skills 的做法完全反过来。它不是把能力塞进模型脑子里而是把能力放在模型“手边”。每个技能独立成模块有自己完整的描述和参数约束模型只需要学会“怎么选技能、怎么填参数、怎么读结果”。这样做的好处我总结成三点解耦技能的新增、修改、下线都不影响其他技能也不动核心 prompt可控每个技能都有严格的输入输出约定模型发挥空间被限制在安全范围内可测技能可以单独测试、记录成功率、埋点分析不用整条链路一起调从工程实践看这是“高内聚、低耦合”思想在 Agent 领域最直观的落地。每个技能自己管好自己的事Agent 核心只负责“判断用户想要什么、该用哪个技能、结果怎么回给用户”。2.2 技能描述决定了模型“会不会用”而不是“能不能用”很多人在定义技能时只写一句话“根据城市查天气”。这种描述看着没什么问题但模型在真实对话中往往会踩坑城市名用户可能说“北京”也可能说“首都”今天是“今天”也可能是“2025-02-18”温度单位是要摄氏还是华氏如果不把这些细节说清楚模型每次都得猜猜错的概率相当高。我后来养成一个习惯每个技能描述必须回答四个问题这个技能是干什么的一句话说清楚用途什么时候应该用它触发条件包括用户说什么话时优先考虑什么时候不应该用它排除条件防止误用参数分别是什么格式、有什么限制包括枚举值、默认值、单位举个例子。一个“查询城市天气”的技能描述里要写明“当用户询问某个具体城市当天或未来几天的天气情况时使用如果用户只提供日期没有城市不要使用此技能先向用户确认城市”。这看起来像是给模型写说明书但这些说明恰恰是让模型在真实场景下做出正确选择的关键。2.3 技能注册表所有技能的“总台账”技能一多管理就成了问题。我建议做一张“技能注册表”不管用 JSON 文件、数据库表还是配置中心至少要维护这几个字段字段名作用示例skill_id技能唯一标识weather_queryname技能名称短查询天气description详细用途说明根据城市和日期查询天气信息parameters参数 Schemacity(必填string)date(可选date)required_scopes需要的权限范围weather:readtimeout_ms超时时间3000enabled是否启用trueversion版本号v1.2.0有了这个注册表你就能清晰地看到整个 Agent 的能力边界。哪些技能高频、哪些技能基本没人用、哪些技能参数总是填错全都能基于这张表做数据分析和持续优化。我见过很多团队一上来就写代码技能东一个西一个出了错都不知道去哪查。先把注册表建好后面所有环节都轻松一大截。3. 核心实现手把手搭一套可复用的 agent-skills 技能库3.1 技能定义的标准结构我先定义一个标准的技能接口。无论后端用什么语言、什么框架这个结构可以保持一致方便后续切换或扩展。# skill_base.py from pydantic import BaseModel, Field from typing import Dict, Any, Optional class SkillInput(BaseModel): 技能输入的标准结构 params: Dict[str, Any] Field( ..., description用户请求中提取的参数key 必须与技能参数 Schema 一致 ) context: Optional[Dict[str, Any]] Field( None, description对话上下文包括用户ID、会话历史、业务上下文等 ) class SkillResult(BaseModel): 技能输出的标准结构 status: str Field( ..., description执行状态只能是 success 或 error ) data: Optional[Dict[str, Any]] Field( None, description技能执行成功时的数据结果 ) error: Optional[str] Field( None, description技能执行失败时的错误信息 ) message: Optional[str] Field( None, description给用户看的可读结果不填则由模型根据 data 生成 ) class BaseSkill: 所有技能必须继承的基类 skill_id: str name: str description: str parameters_schema: Dict[str, Any] {} async def execute(self, params: Dict[str, Any], context: Optional[Dict[str, Any]] None) - SkillResult: raise NotImplementedError这里的重点是parameters_schema。它必须是一个完整的 JSON Schema因为模型要依靠它来理解“该填什么参数、参数是什么格式”。比如{ type: object, properties: { city: { type: string, description: 城市名称例如北京市、上海、广州 }, date: { type: string, description: 查询日期格式为 YYYY-MM-DD默认当天 } }, required: [city] }不少框架如 LangChain、AutoGen都支持直接传入 JSON Schema 让模型自动抽参。但要注意JSON Schema 里的 description 写得好不好直接影响模型抽参数的准确率。比如上面 city 的描述里我特意补了“例如”模型在遇到模糊输入时就更倾向于补全而不是报错。3.2 具体技能实现案例查询天气以最常用的“查询天气”技能为例我写一版完整的实现。这里用了一个公开天气 API 作为示意# skill_weather.py import httpx from skill_base import BaseSkill, SkillResult from typing import Dict, Any, Optional class WeatherSkill(BaseSkill): skill_id weather_query name 查询天气 description ( 当用户询问某个具体城市当天或未来几天的天气情况、温度、降水概率等信息时 使用此技能。注意如果用户只提到‘天气’但没有给出城市名不要使用此技能 请先通过对话向用户确认城市名称。 ) parameters_schema { type: object, properties: { city: {type: string, description: 城市名称例如北京市、上海、广州}, date: {type: string, description: 日期格式 YYYY-MM-DD默认当天} }, required: [city] } async def execute(self, params: Dict[str, Any], context: Optional[Dict[str, Any]] None) - SkillResult: city params.get(city) date params.get(date, today) if not city: return SkillResult(statuserror, error缺少城市参数) # 实际开发中这里调用天气服务 API # 这里用模拟数据演示 mock_data { city: city, date: date, weather: 晴, temperature_min: 12, temperature_max: 20, } return SkillResult( statussuccess, datamock_data, messageNone # 不填让模型根据 data 生成自然语言回答 )这里有个细节值得强调我故意把message留空。为什么因为天气信息怎么呈现有很多种方式——有的用户想知道“适不适合穿外套”有的想知道“明天会不会下雨”。与其用固定文案不如让模型基于结构化数据生成自然语言回答。技能只负责“把数据拿回来”语言表达交给模型这样灵活度会高很多。3.3 技能调度器让模型知道“有哪些技能可用”有了单个技能接下来要让模型知道这些技能的存在。这里需要一个小调度器它负责把技能注册表变成模型能读的格式并调用模型完成“选技能、抽参数、执行、返回”这整个循环。# skill_dispatcher.py import json from typing import List, Dict, Any from skill_base import BaseSkill class SkillDispatcher: def __init__(self, skills: List[BaseSkill]): self.skills {s.skill_id: s for s in skills} def get_skill_prompt(self) - str: 把技能注册表转成模型可读的 format拼到 system prompt 中 lines [] for skill in self.skills.values(): lines.append(f## 技能 {skill.skill_id}{skill.name}) lines.append(f用途{skill.description}) lines.append(f参数 Schema{json.dumps(skill.parameters_schema, ensure_asciiFalse)}) return \n\n.join(lines) async def dispatch(self, user_input: str, context: Dict[str, Any]) - str: # 1. 构造 LLM 输入 system_prompt ( 你是一个技能调度器根据用户的请求选择合适的技能并提取参数 然后调用技能获取数据最后以自然语言回复用户。 可选技能如下\n\n self.get_skill_prompt() ) # 2. 调用 LLM 得到技能名和参数这里用伪代码表示 # response await llm.chat( # systemsystem_prompt, # useruser_input # ) # 让模型以 JSON 格式返回例如 {skill_id: weather_query, params: {city: 北京}} # 3. 根据返回结果执行对应技能 # skill self.skills[response.skill_id] # result await skill.execute(response.params, context) # 4. 把 result 交给 LLM 生成最终回复 # final_reply await llm.chat( # system根据技能返回数据生成自然语言回复, # userf原始请求{user_input}\n技能数据{result.data} # ) # return final_reply # 以上为示意流程 pass实际开发中你完全可以不自己写调度循环直接用 LangChain 的 Tool/Function Calling、Claude Skills、OpenAI Function Calling 这些现成机制。但理解上面这个流程很重要——不管底层用哪个框架本质都是“给模型看技能说明 → 模型选技能并抽参 → 执行代码 → 模型整理结果”。把底层抽象理解透了换框架只是换 API 的问题。3.4 补充一段技能注册与动态加载技能一多总不能每次上新技能都改代码发版。所以我在项目里加了动态注册机制把每个技能类放到指定目录启动时扫描目录并自动加载。这样加新技能只需要写一个新文件放到目录里就能立即生效不用动主程序。# skill_loader.py import os import importlib.util from skill_base import BaseSkill def load_skills_from_dir(directory: str): 从指定目录动态加载所有技能定义 skills [] for filename in os.listdir(directory): if not filename.endswith(.py) or filename.startswith(__): continue module_path os.path.join(directory, filename) module_name filename[:-3] spec importlib.util.spec_from_file_location(module_name, module_path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # 遍历模块中的类找到 BaseSkill 的子类并实例化 for attr_name in dir(module): attr getattr(module, attr_name) if ( isinstance(attr, type) and issubclass(attr, BaseSkill) and attr is not BaseSkill ): skills.append(attr()) return skills这个动态加载方案我在实际项目中跑得很稳。有个很重要的细节技能类初始化不要加载重资源。真正的数据库连接、API 客户端等放懒加载或者用依赖注入避免目录扫描时把整个服务拖慢。我以前吃过这个亏技能多了之后启动要好几秒排查半天才发现是每个技能都建立了一个外部连接后来改成按需初始化就好了。3.5 参数抽取的质量问题与几个实测经验参数抽取是 Agent 应用里非常容易出现问题的环节。我看到了几个高频问题以及对应的解决办法时间类参数用户说“后天”模型得算成具体日期。建议在参数验证阶段把相对时间先解析成绝对时间别让后端去猜“后天”是几号。这个前置处理能省掉大量接口端的麻烦。模糊地点用户说“回龙观”要不要自动补成“北京市回龙观”看业务需求。如果是天气查询可以自动补如果是订机票、填收货地址就一定要跟用户确认。宁可多一次确认不要直接猜错。缺失必填参数不要直接报错让用户重说。引导式追问效果好得多。比如“请问您想查询哪个城市的天气呢”这句追问比“缺少参数 city”用户体验好十条街。我实测过一段时间发现把追问逻辑写进技能描述比写在代码里更有效。例如在天气技能的 description 中写“如果用户没有提供城市名请询问用户希望查询哪个城市的天气”模型在抽参失败时的表现会明显更自然不再是生硬的报错。4. 实操过程中最容易踩的坑与排查清单4.1 技能描述写得模棱两可模型频繁误选技能这是出现频率最高的问题。比如两个技能“查询天气”和“查询景点天气”描述都写了“天气”模型就很容易混淆。解决方法很简单把技能的边界条件写清楚。在“查询景点天气”的描述里加一句“当用户提到某个景区、公园、景点并要求查询该地点天气时使用此技能而不是常规城市天气技能”。看似多了一句废话但对模型来说这就是最重要的区分信号。4.2 技能内部异常没有兜底整条链路直接炸掉技能内部调用外部 API 时网络超时、返回格式变了、字段缺失都是家常便饭。如果这些异常不处理Error 就会一路抛到大模型那里模型要么胡编一个结果要么直接说“系统错误”。我的做法是在技能执行的最外层捕获所有异常并转换为 SkillResult(statuserror)。这样模型至少知道“这个技能没执行成功”可以触发后续的补偿动作比如让用户稍后重试。4.3 上下文污染技能 A 的结果被技能 B 误用多技能对话场景有个经典 bug用户先问“北京天气怎么样”再问“那上海呢”调度器把“上海”正确抽出来执行了但 LLM 在生成回答时还把上次的“北京天气数据”也带上了结果答非所问。解决办法是在每次技能调度后把上一次的技能执行结果从上下文里清理或标记。我习惯在上下文中用一个独立的last_skill_result字段记录当前轮技能结果模型可以引用它来生成回复下一轮调用新技能时这个字段就会被覆盖不会带到下一次。用 LangChain 这类框架时要注意 Message 历史里可能会残留旧工具调用的结果必要时手动裁剪历史消息只保留最近的几轮对话。4.4 常用排查清单速查表问题表现可能原因排查思路模型总是选错技能技能描述没有区分度检查相邻技能的 description补充“不使用条件”参数频繁抽取错误JSON Schema 中描述太简略补全必填/可选、格式示例、枚举值技能执行成功但回复内容奇怪LLM 生成阶段上下文混乱检查是否把上一轮技能结果混入本轮新技能不生效动态加载目录扫描异常确认文件命名、类命名是否规范重启是否生效技能调用耗时过长外部 API 慢或者技能内部逻辑太重设置合理超时考虑缓存或异步处理4.5 性能与成本控制的三个心得技能体系做得再好如果每次调用都让大模型跑好几轮Token 成本和响应延迟都会非常难看。我的经验是给调度器加“直接命中”缓存比如“北京天气”“上海天气”这种高频查询可以把参数抽取结果缓存起来下次直接跳过 LLM 抽参这一步大幅缩短链路。实测一些高频固定句式能省下 30% 以上的 Token 消耗。技能结果设置 TTL 缓存天气这种短时效数据1~2 分钟内重复查询可以直接返回缓存数据不必每次重新请求外部 API。小模型做调度大模型做总结技能选择和参数抽取这类“结构化任务”用参数更小的模型就能完成得很好没必要每次都让最强的模型上。只有最后生成自然语言回复时才需要更强的模型能力。这种组合方式在成本上能拉开好几倍差距。5. 再往前走一步agent-skills 的扩展方向技能体系搭好之后扩展空间其实比想象中大很多。我自己正在尝试几个方向简单分享一下。技能编排。单个技能处理简单请求没问题但真实业务往往是“查了天气之后推荐穿衣搭配”“查了航班之后再帮用户订酒店”。这时候要把多个技能串成流程需要一套编排能力用代码或者 DSL 定义技能调用顺序和条件分支。这个编排层一旦做好Agent 能处理的场景复杂度会明显上一个台阶。技能可观测性。每个技能的成功率、平均耗时、参数分布、用户反馈都是非常有价值的数据。我在实际项目中给技能加了一套 Filter统一记录日志和埋点然后丢到监控系统里做报表。这样做的好处是当某类请求的满意度下降时你能快速定位是不是某个技能的某个环节出了问题。技能社区化。当技能库成长为上百个技能时你会希望团队里的不同人分别维护各自的技能像开源项目一样协作。每个技能独立仓库、独立版本、独立发布共享一份技能注册表这套模式对团队协作效率的提升非常明显。我个人在实际操作中的体会是agent-skills 的价值不在于它多复杂而在于它把 Agent 从一个“一坨 prompt 撑起来的黑盒”变成了“一堆可管理、可测试、可观测的功能模块”。哪怕你暂时不引入任何重型框架只按照这套思路整理一下技能的边界、描述和参数你的 Agent 表现都会出现肉眼可见的提升。先动手把一个最简单的技能跑通再逐步扩展这条路走起来最稳。
返回列表