ARTICLE DETAIL

资讯详情

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

Agent技能体系实战:告别Prompt膨胀,构建可扩展的AI Agent

Agent技能体系实战:告别Prompt膨胀,构建可扩展的AI Agent 最近一个月我大部分时间都泡在Agent开发上。从最开始用Prompt硬怼到后来把能力拆成一个个独立技能注册进系统整个思路转变带来的效果提升非常明显。今天想聊的这套“agent-skills”体系就是基于这段实践沉淀下来的一套方法。如果你正在做AI Agent相关的东西无论是个人的自动化助手、企业内部的知识库机器人还是面向C端的复杂任务执行器只要发现“提示词越写越长、模型越来越懵、效果越来越差”那这篇文章应该能给你一些参考。我会把技能体系是什么、为什么值得做、怎么设计怎么落地、以及我实际踩过的坑一次性讲清楚。1. 别再用Prompt硬撑了结构化技能体系的必要性1.1 当Prompt膨胀到失控时问题就来了我先描述一个场景。早期做Agent的时候最直接的思路就是把所有能力描述写进系统提示词里“你可以搜索网页”“你可以调用计算器”“你可以查天气”“你可以读写文件”“你可以调用代码解释器”……一开始十几个功能还好等规模上到几十个的时候Prompt随便就是上万字。然后你会遇到几个非常头疼的现象。第一个是模型开始“丢功能”。它明明“知道”自己有某个工具但在具体任务里怎么都想不起来调用。我测试过一个案例系统接了十几个工具模型在需要查询数据库的时候愣是绕路去读了本地缓存文件结果数据都是旧的。第二个是功能之间的边界开始模糊。你定义了“日程查询”和“日程创建”两个功能模型可能把“帮我看看明天几点开会”理解成“创建一个日程”然后写了个根本不存在的事件进去。第三个是维护成本飙升。每加一个能力你要重新测一遍所有旧功能会不会被新功能干扰Prompt的调优变成了打地鼠。我意识到Prompt不是不能做而是它适合描述“how to behave”不适合描述“what capabilities are available”。能力这个东西应该被结构化地表达、注册、调度而不是塞在自然语言里让模型自己猜。所以agent-skills的思路就是把Agent的能力从Prompt里剥离出来形成独立的、可描述、可组合、可调度的技能单元。1.2 什么是技能Skill它和工具Tool的关系很多文章里Skill和Tool是混着用的但在我实践的过程中两者必须分清。Tool是原子操作比如“发送HTTP请求”“执行一段SQL”“写一个文件”。Skill则是面向任务的完整能力它通常是多个工具调用的组合再加上前置条件、后置处理、参数校验、异常恢复这些逻辑。举个例子。“发一封带附件的邮件”就是一个Skill。它内部要调用联系人查询Tool、草稿生成Tool、SMTP发送Tool还要处理附件大小超限、收件人格式错误这些情况。如果只暴露Tool给Agent它就需要在对话中自己决定按什么顺序调用哪些Tool出错了怎么办。这等于把实现逻辑交给了模型的自由发挥结果必然是偶发性的失败——同一句话问十次可能三次成功七次出错或者每次走的路径都不一样。Skill则把这一切收敛起来。Agent只需要知道“有个技能叫发送邮件输入是收件人、主题、正文、附件路径”剩下的复杂操作全在技能内部完成。这种封装带来的直接好处是模型要做的事情更简单错误面更小行为也更可预测。1.3 这套体系到底解决了哪几类问题总结下来agent-skills解决的核心问题有三类。第一类是能力发现。Agent得知道自己有哪些技能、各自是用来干什么的、什么时候该用哪一个。结构化技能注册表天然解决了这个问题只要Agent能读到技能清单就不会出现“想不起来调用”的情况。第二类是能力执行。技能内部的自有逻辑保证了执行过程是稳定可控的。参数校验、前置检查、调用顺序、异常处理都在代码层面写死而不是依赖模型的临场发挥。第三类是能力迭代。新增一个技能不需要改动系统提示词不需要重新跑全部回归测试。只要新技能不和其他技能的触发条件强冲突它就是独立可部署的。这点在多人协作开发Agent时尤其重要不然每加一个功能都要互相等、互相牵制。2. 技能体系设计从业务任务到技能清单的拆解方法2.1 自顶向下先用用户意图聚类而不是按功能堆叠在动手写代码之前最需要想清楚的是技能怎么划分才对。我一开始犯过的错误是“按功能类型分”搜索类、计算类、文件类、数据库类、API调用类……看起来很整洁但到了实际使用阶段你就会发现模型根本不知道该怎么选。因为用户的表达从来不是“我要调用搜索功能”而是“帮我找一下去年Q3的销售数据”。后者可能涉及数据库查询、数据清洗、生成报告、保存文档横跨了我划分的好几个类别。正确的做法是自顶向下从用户的实际意图出发。你把所有你能想到的任务场景写下来比如“查询经营数据”“生成周报”“安排日程”“发送通知”“整理文档资料”然后对每个场景问三个问题需要哪些信息需要经过哪些处理步骤最终期望产出什么答完之后一个场景往往就对应一个或几个候选技能。2.2 技能的粒度多大算合适多小算碎片化技能粒度的拿捏是整个设计里最微妙的部分。粒度太粗技能内部变成一个大杂烩什么都能干但又什么都干不好而且复用的可能性极低粒度太细技能数量和触发条件爆炸Agent光做技能选择就晕了。我给一个比较实用的经验判断标准一个技能应该能回答“做什么”和“怎么做完”并且它的输入输出范围足够窄窄到你可以用一段话准确描述它。如果你发现描述一个技能需要超过三句话才能说清楚边界那大概率它应该拆成两个或多个子技能。反过来如果两个技能经常在同一个任务里被连续调用而且顺序固定那它们可以考虑合并成一个组合型技能。比如一开始我把“解读销售数据”和“生成数据图表”分成两个独立技能。实测中发现几乎每个请求都是先解读再画图命中率接近百分之百而后合并成“销售数据分析与可视化”。合并之后效果更好因为这本来就是用户脑子里一个完整的预期拆成两步反而制造了中间状态的传递问题——分析结果放在内存里还是临时文件怎么传给下一个技能这些都是额外成本。2.3 技能描述怎么写模型才真正看得懂技能描述的重要性不亚于技能本身的实现。在Skill调度中模型是通过描述来选择技能的描述写得模糊模型选错就是必然的。参考我大量测试后的经验一份好的技能描述应该包含四类信息能力概述、适用场景、关键参数说明、使用限制。能力概述要一句话直击本质例如“生成一份包含趋势分析和结论建议的PDF格式销售周报”。适用场景要给出正例和反例例如“适用于需要用自然语言查询近30天销售数据的场景不适用于需要逐笔明细数据的场景”。关键参数要说明每个输入字段的含义和可选值。使用限制说明什么情况下不能调用这个技能比如依赖外部数据库不可用或者需要管理员权限。描述的高级技巧是“用模型的语言写描述”。什么意思呢模型对自然语言的理解能力远强于对结构化标签的理解。与其写“data_range: str”不如写“data_range: 本次分析的时间范围格式为YYYY-MM-DD支持“近7天”“本月”“上季度”这样的自然语言表达由调用方提供原始文字”。这样模型在提取参数时能直接把用户的原始表达映射过来不用再做一轮“翻译”。3. 注册、加载与调度让Agent知道自己“会什么”和“怎么用”3.1 技能注册表的结构设计技能注册表是整个agent-skills体系的地基。它告诉Agent两个核心信息系统里有哪些技能以及每个技能怎么被调用。我用的是一份JSON格式的注册表每个技能包含skill_id、name、description、parameters_schema、required_permissions、entry_point。其中skill_id是全局唯一标识name是给Agent和日志看的人类友好名description是按照上一节标准写的详细描述parameters_schema是JSON Schema格式的参数说明用于校验和引导模型提取参数required_permissions是这个技能需要的权限级别比如“只读”“读写”“执行外部命令”entry_point指向技能的实际实现模块路径。这个注册表本身要设计成语义化加载的也就是说程序启动时扫描注册表目录每个技能一个JSON文件自动读入内存。增加新技能只需要放一个文件进去不需要改主程序代码。我当时把注册表也暴露给了模型查询接口任何一次会话开始前Agent都会先调用“列出所有可用技能”这个内置方法拿到当前最新的技能清单。这一步虽然有点笨但确实有效它保证了Agent永远不会带着过期的技能认知开始工作。3.2 动态加载技能模块运行时发现与热更新注册表只是元数据真正的技能实现需要动态加载。我用的语言是Python所以优先考虑了模块热加载方案。具体来说每个技能实现是一个Python文件内部定义一个继承基础类的Skill类实现execute方法。执行入口是这样的import importlib class SkillRegistry: def __init__(self): self.skills {} self.loaded_modules {} def load_skill(self, skill_meta: dict): module_path skill_meta[entry_point] module importlib.import_module(module_path, packageskills) skill_class getattr(module, Skill) skill_instance skill_class(skill_meta) self.skills[skill_meta[skill_id]] skill_instance return skill_instance热更新怎么做呢监听注册表目录的文件变化事件一旦有新增或修改就重新加载对应的模块注销旧的技能类。这样在运行中的Agent进程里添加新技能完全不需要重启。开发联调阶段的体验比“改一行代码重启一次服务”好了不止一个档次。3.3 调度逻辑意图识别、技能匹配、参数提取、执行、结果回填调度流程是技能体系运转的核心。我把整个链路分成五个环节意图识别、技能匹配、参数提取、执行、结果回填。意图识别的输出不是自然语言而是一个“候选技能列表”。这里我不建议让模型一次性决定用哪个技能因为一次判断出错就全盘皆输。更稳的方式是让模型先给出两到三个候选再通过一个轻量级的评分函数做最终确认。评分函数可以考虑用户表述和技能描述之间的语义相似度、历史任务中该技能的命中率、以及技能自身标注的优先级。参数提取是最容易出问题的一环。用户说“查一下上个月北京的销售数据”技能是“销售数据查询”参数列表是time_range、region、metric。模型需要完成的是从这句话里把三个参数都抠出来这是个典型的“结构化信息抽取”任务。我强烈建议在这个环节单独给模型一次机会用独立的LLM调用做参数填充而不是和执行决策混在一个调用里。混在一起的结果往往是技能选对了参数没填全执行阶段只能靠默认值硬跑跑出来结果用户根本不想要。执行阶段就是把参数传进技能实现得到结果。这一步要注意的是超时控制。某些技能会调用外部API外部服务可能长时间无响应如果不设超时整个Agent流程就被卡死了。我给每个技能设置了独立的超时时间数据查询类的给30秒文件处理类的给10秒达到阈值直接返回超时错误并触发降级回复。结果回填要做的不是简单把执行结果原样返回给用户而是先做一次“结果整理”。执行结果通常是结构化数据或中间产物需要翻译成用户能看懂的话。这个翻译动作也建议用独立LLM调用完成并且把原始结果和整理后的回答放在两个字段里方便后续追踪问题到底出在执行还是出在语言组织。def run_skill(self, skill_id: str, params: dict): skill self.skills.get(skill_id) if not skill: raise SkillNotFoundError(skill_id) if not skill.validate_params(params): raise ParamValidationError(skill_id, params) task asyncio.create_task(skill.execute(params)) try: result await asyncio.wait_for(task, timeoutskill.timeout) except asyncio.TimeoutError: raise SkillTimeoutError(skill_id) return skill.post_process(result)4. 技能实现细节从手写代码到工具调用的关键路径4.1 一个完整技能的代码骨架说再多理论不如看一个完整的技能实现。下面这个技能负责“获取指定股票的实时行情”它是系统里最简单的一类技能非常适合作为参考模板。import json import requests from typing import Dict, Any from skills.base import Skill class Skill(Skill): def validate_params(self, params: Dict[str, Any]) - bool: symbol params.get(symbol) if not symbol or not isinstance(symbol, str): return False if not params.get(period, day).lower() in [day, week, month]: return False return True async def execute(self, params: Dict[str, Any]) - Dict[str, Any]: symbol params[symbol].upper() period params.get(period, day).lower() url fhttps://api.example.com/v1/quote resp await self.http_get(url, params{symbol: symbol, period: period}) data resp.json() return { symbol: symbol, period: period, price: data[current_price], change_pct: data[change_percent], timestamp: data[last_update] }这个骨架体现了几个优秀实践。validate_params和execute分离让校验逻辑独立于业务逻辑。真正执行的时候技能内部所有的网络请求都应该异步化避免阻塞Agent主线程。返回的数据要精简只保留Agent生成回复真正需要的字段别把整个API响应一股脑倒给模型。4.2 基础能力库每个技能都绕不开的通用函数技能多了以后你会发现大量技能都在做相似的事情调用外部HTTP API、读取文件、查询数据库、调用命令行工具。每实现一个技能就把这些逻辑重复写一遍是不可能有好的维护性的。所以有必要沉淀一个基础能力库把通用动作封装成可复用的函数。我维护的基础能力库目前包括async_http_get/post封装了超时、重试、User-Agent管理、DBConnector统一处理连接池、事务、查询结果转JSON、FileOperator限路径范围内的读写操作防止路径穿越、CommandRunner执行白名单内的命令行并设超时和输出截断。有一点要特别提醒基础能力库的接口设计一定要从“技能需求”出发而不是从“通用性”出发。我看到过有人把基础库设计成了一个超级抽象框架各种设计模式、多层继承结果写技能的人为了用其中一个函数要搞懂四层类的继承关系。这完全是本末倒置。基础库的核心目标是让技能实现尽量短小精悍而不是展示设计能力。4.3 技能内部的状态管理和上下文隔离技能执行过程不是永远一次就成功的。外部API会超时数据库连接会断开文件可能不存在。好的技能需要具备一定的故障恢复能力。我在技能基类里实现了一个简易的状态机READY、RUNNING、SUCCEEDED、FAILED、RECOVERED、TIMEOUT。当技能执行抛出异常时基类会捕获并检查该技能是否声明了retry_policy。retry_policy里面定义了最大重试次数、每次重试前的等待时间、以及哪些异常类型允许重试。网络超时这种瞬时错误值得重试参数错误这种永久性错误重试一万次也没有意义。重试N次仍失败后技能返回一个结构化的失败原因Agent拿到这个原因可以决定是否换一个技能来兜底。上下文隔离也很重要。每个技能执行时应该有独立的上下文对象里面包含输入参数、中间状态、临时文件目录、日志记录。技能之间不能直接共享上下文只能通过返回值传递数据。这样做的原因很简单避免了并发场景下不同技能互相污染数据的灾难性情形。我遇到过两次诡异Bug查到最后都是全局变量被同时执行的两个技能改了从那以后上下文隔离就成了硬性规范。5. 实测中的翻车现场技能调度最容易踩的四个坑5.1 技能误判用户只是随口一提Agent却真的调用了实测下来最容易出问题的就是技能误判。典型场景是用户说“我在想是不是应该做一个数据分析类的产品”这明显是用户的思考过程不是下达的执行指令。但Agent听到“数据分析”四个字可能就直接调用了数据分析技能生成一份报告出来。这个问题怎么办我在技能描述里明确加入了“触发条件”和“不触发条件”两个字段并且要求模型在决策时必须同时核对这两项。更重要的是增加了一个“确认前置”机制当技能的触发置信度在70%到90%之间时不在内部直接执行而是生成一条确认信息“我理解你需要做XX是否现在就开始”等用户明确说“开始”之后再执行。这个机制的副作用是降低了一点自动化程度但大幅度减少了误执行带来的恶劣后果。在用户体验上多一次确认远远好过执行一个用户根本不想要的操作。对于高权限技能比如“发送邮件”“修改数据库”“删除文件”我强烈建议无条件开启确认前置。5.2 参数错位时间范围漏了单位模型自己脑补数值参数提取的失败案例里时间类参数错得最离谱。用户说“帮我查本周的销售”模型的参数提取结果可能是“time_range: 2025-01-01, 2025-01-07”把整个季度开头当成本周。为什么会出现这种错误因为“本周”是一个相对时间表达模型需要先确定今天是什么日期才能推算本周的起止日。但很多模型的训练数据里没有当前日期这个概念除非你在参数提取提示词里塞进系统当前时间。我的解决方案是双管齐下。第一参数提取之前系统先自动注入当前日期时间戳到模型上下文。第二Schema层面强制校验如果time_range的格式不符合YYYY-MM-DD或者起止日期差值超过30天校验直接失败并触发一次重新提问“抱歉我需要的查询范围是在最近30天内请确认你的时间范围。”实测下来这两步把时间参数的错误率从大概15%降到了3%以内。5.3 上下文污染上一个技能的执行细节干扰了下一次决策Agent往往是在同一个会话里连续处理多个任务的。用户可能先让你查一个数据然后让你根据这个数据写一封邮件再然后让你把邮件内容更新到公司维基上。这三个任务对应三个不同的技能按理说彼此独立。但实际情况是第一个任务的中间日志、临时文件路径、甚至错误堆栈都留在上下文窗口里模型在决策第三个任务时可能会被这些无关信息干扰。我采取的方案是把每个技能调用封装成一个闭包式的执行上下文在执行结束后将这段上下文内容压缩成一个“执行摘要”。摘要里只保留任务目标、执行结果、最终产物位置其他所有中间过程全部丢弃。这样下一个技能做决策时能看到的只是上一个技能的输入输出摘要而不是几十行原始日志。举个例子“查询销售数据”技能的摘要可能只有一行“技能query_sales执行成功产出PDF报告链接/tmp/reports/sales_2025_06.pdf耗时2.1秒。”后面的“生成邮件”技能只需要知道有这个PDF存在知道它的路径就够了完全不需要知道查询过程里的SQL语句、API重试了几次这些细节。5.4 技能组合时的冲突两个技能同时都想抢占一个资源当Agent开始连续调用多个技能时资源竞争的问题就出现了。常见的冲突场景是技能A申请了临时目录/agent_tmp在后台长时间运行技能B同时启动也往/agent_tmp里写文件结果A写到一半发现文件被覆盖。这类问题的本质是缺少资源隔离和锁机制。我现在的方案是每个技能执行时分配一个独立的工作子目录目录名是skill_id加随机短码比如/agent_tmp/send_email_k3j9。技能只能访问自己子目录内的文件访问其他目录直接拒绝。对于某些需要全局独占的资源比如“修改总配置文件”“重启后台服务”技能基类里单独提供acquire_global_lock声明同一时间只允许一个技能持有全局锁其余技能等待或超时返回忙。这套机制引入以后技能组合执行的成功率从原来的85%出头稳定提升到了97%以上。剩下的3%大多来自外部服务的偶发故障已经不在技能内部能解决的范畴。6. 技能的可观测性与自省别让Agent成为一个黑盒6.1 技能调用日志记录每一次“思考”与“执行”Agent开发里最容易被忽略、但后期最救命的就是完整的调用日志。我的日志体系分三层决策日志、执行日志、结果日志。决策日志记录的是模型每次调度技能时的思考链包括候选技能列表、每个候选的置信度、最终选中的技能ID、以及选中的理由从模型返回的思考文本里提取。执行日志记录的是技能运行过程中的关键节点比如参数校验通过、开始调用外部API、API返回状态码、重试了几次。结果日志记录的是技能最终的返回内容和整理后的用户回复。这套日志的价值在出问题时体现得淋漓尽致。有一次用户反馈Agent答非所问我翻日志发现模型决策时选择了“生成报告”技能而不是“查询数据”技能置信度分别是0.72和0.68就差一点。这一眼就定位到是技能描述边界不够清晰我调整了描述之后这个案例就再没出现过。6.2 技能回放与离线评测改了一个技能敢不敢直接上线还有一个关键实践是离线评测。Agent的技能调度涉及模型行为模型是非确定性的你不能只看一两个案例通过了就认为改了描述没问题。我的做法是准备一个评测集里面每条样本是一条用户请求加上期望调用的技能ID和期望参数。任何一次技能描述或调度逻辑变更都先在评测集上跑一遍。评测指标我用的是三个技能选择准确率、参数提取准确率、任务完成质量评分。前两个可以自动化计算第三个需要结合人工抽查。我给自己定的标准是技能选择准确率不能低于95%参数提取准确率不能低于90%否则变更不能上线。不要觉得95%很高真正测试下来一个有经验的人写的技能描述配合清晰的参数Schema达到这个标准是可行的。每次运行评测还会生成一份错误的case列表我会逐个去看错在哪。大部分错误集中在描述中的反例不够充分、参数说明存在歧义这两个原因。修掉之后重新评测通常就能过线。6.3 技能健康度监控上线只是开始持续调优才是常态技能上线之后还有一个长期工作要做健康度监控。我给每个技能挂了三个指标调用成功率、平均耗时、用户反馈负面率。这些指标按日聚合出现异常时自动告警。调用成功率下降通常意味着技能依赖的外部服务出问题了或者是参数提取的质量下降导致校验大量失败。平均耗时上升要排查是外部API变慢了还是技能内部逻辑出现了意外的循环等待。用户反馈负面率是最直接的信号用户在原回答上点了“不满意”这个数据必须和技能调用日志关联起来看。我把健康度数据和技能描述做了一次联动当一个技能的负面率连续三天超过阈值系统会自动在技能描述开头插入一条警告字段“该技能近期用户反馈较差使用时请确认用户真实意图若不确定建议换用其他更合适的技能或直接询问用户”。这看似是个笨办法但实测确实起到了作用——模型读到这个警告后会倾向于多问一次确认而不是无条件执行间接减少了坏结果的发生。说完这些说说我现在的体会把Agent的能力从一段段Prompt改成一套结构化、可注册、可调度、可观测的skills体系这个过程最大的感受是开发心态完全变了。以前每改一处Prompt都提心吊胆怕影响别的功能现在新增技能就像往工具箱里放一把新螺丝刀独立、清爽、可测试。技能体系的建立也让我终于敢把Agent放到生产环境里处理真实的用户请求因为每一个技能调用都有日志、有监控、有回放出了问题能快速定位。如果你也正被Agent的不可控性搞得焦头烂额不妨先从拆一个最常用的功能开始把它做成第一个技能搭好注册表和调度链然后再慢慢把其他功能迁移进来。不要想着一步到位先跑通一个小闭环你会很快感受到这种方式的差别。
返回列表