直接说结论:干了一年多Agent落地项目,我最深的体会是——模型选得再好,跑不起来的大多数原因都出在技能层。所谓“agent-skills”,说穿了就是给大模型配一套它真正“会用”的工具箱。很多团队把GPT-4级别的模型接进来,发现它像个高分低能的新员工,道理都懂,一上手就懵。问题不在模型,在没人教它怎么用工具、怎么拆任务、怎么把结果收回来。
这篇文章我用自己的实战经历,把Agent技能体系的拆解思路、技能定义规范、实操步骤、踩坑记录全部摊开来讲。适合正在做Agent应用开发、搞大模型落地、或者准备给现有系统接AI能力的开发者。如果你只是想跑个demo玩一玩,也能拿走一套至少能用的模板。
1. 为什么需要技能层:从模型能力到业务价值的最后一公里
1.1 Agent的瓶颈从来不在模型本身
先说一个现象。同一个基座模型,有人用它做出来的东西像私人助理,有人做出来的就是个高级聊天机器人。差距在哪?就在技能层。
我在实际项目里见过不少团队,一开始觉得只要模型够聪明,给它一个系统提示词,它就能自己搞懂一切。真跑起来才发现不是这么回事。模型确实聪明,但它对“你的系统里有什么能力可用”一无所知。你不告诉它有个函数能查库存,它永远不知道可以调这个函数;你告诉它了但没说清楚什么时候调、参数怎么填,它就会在错误的时间调、填错误的值。
这就是技能层要做的事。一套合理的技能体系,本质上是给模型写清楚一份“岗位说明书 + 操作手册”:在什么场景下用哪个技能、技能需要哪些输入、执行完了之后输出长什么样、失败了下一步怎么办。
我用一个生活类比来解释,你招了个实习生,脑子很聪明(基座模型),但你不可能指望他第一天就什么都会。你得给他培训手册、标准作业流程、操作规范。这堆东西就是技能层。没有这层,实习生只能靠猜,而Agent靠猜的下场就是随机发挥。
1.2 技能体系要解决的四个真实问题
我在设计技能层时,核心就盯着四个问题,每个都吃过亏:
意图到动作的映射。用户说“帮我查一下上个月的销售数据”,模型知道需要查数据,但它不知道你的系统里有个报表查询接口,更不知道这个接口需要传哪个部门ID、哪个时间粒度。技能定义的核心功能之一,就是把用户的自然语言意图翻译成可执行的函数调用。
参数的抽取和补全。这是最容易被低估的坑。模型要从对话上下文、历史记录、甚至外部文档里抽出参数。比如用户说“下周开周会的时候提醒我”,模型得自己推理出:下周一是几号?周会默认是几点?“提醒”应该用日历技能还是IM消息技能?参数抽不准,后面全白费。
结果的标准化返回。如果技能返回的是整段自由文本,模型读起来费劲、提取信息容易出错,而且大量无关信息会污染上下文。技能输出必须结构化:状态码、数据体、可读消息、下一步建议。这是我后来总结的铁律。
失败后的兜底恢复。技能调用不是100%成功的。接口超时、参数缺漏、业务规则冲突,都会让技能执行失败。失败之后模型是硬着头皮瞎编,还是优雅地告诉用户“我查不到,但我可以帮你做X”,完全取决于技能层有没有建立兜底机制。
这四个问题如果不解决,AI能力就是空中楼阁。解决了,模型才能真正变成一个“能干活”的系统组件。
2. 技能结构设计:一份能跑起来的技能定义长什么样
2.1 技能元信息:一个技能该有的六个要素
我把一个技能定义拆成六个部分。这套结构我在多个项目里复用,效果稳定,新手可以直接照抄。
技能名称(name):机器读的标识符,全小写加下划线,比如query_sales_report、send_reminder。命名要短、要有区分度,避免两个技能名称太像导致模型混淆。
技能描述(description):这部分是给模型看的,不是给人看的。必须写清楚触发场景、动作目标、边界条件。我见过太多人把description写成给领导汇报的功能清单,模型看了完全不知道怎么触发。
参数声明(parameters):用JSON Schema风格定义。每个参数要有类型、是否必填、枚举值、默认值、描述。参数描述里还要写明白“这个值从哪里来”,比如“从用户最近一条消息中提取时间,格式为YYYY-MM-DD”。
执行函数(function):实际跑业务逻辑的代码。可以是HTTP调用、数据库查询、内部服务调用。执行函数要幂等、要可控、要有明确的错误输出。
返回结构(output):固定格式的返回体。我用的标准是{code, data, message, next_actions}。data里只放必要字段,message是人读的提示语,next_actions告诉模型接下来可以建议用户做什么。
使用示例(examples):给模型一两个few-shot样本,展示“什么输入→调什么技能→传什么参数”。实践经验是,配了示例的技能触发准确率能提升30%以上。
2.2 description怎么写才不会被模型无视或误触发
这是整个技能定义里最容易翻车的地方。description写得不好,会出现两种极端:一是模型从来不调这个技能,二是模型乱调这个技能。
先看一个反例:
description: "查询销售数据的工具,需要传入部门ID和时间范围,返回销售报表"这个描述的问题在于没有说清楚“什么时候用”。模型看完只知道这个工具存在,但不知道用户说什么话时应该触发它。比如用户说“帮我看看华东区卖得怎么样”,模型可能不会触发,因为描述里没有“华东区”这个概念来引导它。
我用的推荐模板是这样的:
description: "当用户需要查看销售数据、报表、业绩或销售额时使用。支持按部门、按时间范围筛选。当用户只是询问销售额概念而不需要真实数据时,不要使用本技能。"模板拆开就是:触发场景 + 排除场景。第一句告诉模型什么情况启动,第二句告诉模型什么情况别碰。实测下来,加了排除场景之后,误触发率能降一半。
2.3 参数设计的三个原则和一大坑
原则一:能默认就默认。参数越多,模型抽取出错概率越高。比如提醒技能里的“提醒方式”,直接默认“站内消息”,用户不特别说就不用传。
原则二:参数描述要写清来源。光写“time: string类型”没用,模型不知道time从哪来。要写成“time: string类型,从用户消息中解析出时间点,格式HH:MM,若用户未指定则默认为09:00”。
原则三:枚举值必须写全。模型的特性是,你给它的选项不够,它就自己发明。参数里如果是枚举类型,把能想到的值全列上,再留一个“其他(other)”兜底,防止它自由发挥。
一个大坑:别把校验逻辑寄托在模型身上。模型抽出来的参数永远是概率结果,没有100%准确一说。执行函数里必须自己再校验一遍:类型对不对、范围合不合理、必填有没有缺。我见过安全事故,就是模型把日期填成了未来某天,直接导致定时任务排错。参数校验必须由代码兜底,不能相信模型。
3. 实操:从零搭建一套最小可用的Agent技能体系
3.1 场景定义与技能清单:以个人知识助手为例
为了让你能直接照着抄,我选一个典型场景:做一个个人知识助手,它需要具备三类能力——读文档、查数据、主动触达。
对应到技能设计,我拆成三个技能:
文档检索技能:根据用户问题,在知识库中找到最相关的文档片段并返回摘要。对应的是“读”的能力。
表格查询技能:对结构化数据执行查询,比如“上个月各渠道的用户增长情况”。对应的是“算”的能力。
定时提醒技能:把用户的自然语言时间描述转换成定时任务并注册。对应的是“主动触达”能力。
这三个技能刚好覆盖Agent最常见的三类动作,而且复杂度递增,拿来练手最合适。
3.2 技能定义落地:三个技能完整配置文件
第一个文档检索技能。这是最常用的,也是最容易写坏的。我的定义如下:
{ "name": "knowledge_retrieval", "description": "当用户询问文档内容、资料、报告、知识库内的问题时使用。从内部知识库检索与问题最相关的段落并返回摘要。当用户只是闲聊或询问常识且与知识库内容无关时,不要使用。", "parameters": { "query": { "type": "string", "required": true, "description": "要检索的问题或关键词,从用户最新一条消息中提取,可保留原话" }, "top_k": { "type": "integer", "required": false, "description": "返回相关片段数量,默认3,最大5" } }, "output": { "code": "int", "data": { "fragments": [ {"doc_id": "string", "summary": "string", "relevance_score": "float"} ] }, "message": "string", "next_actions": [] }, "examples": [ { "user_input": "我们上周的周报里关于客户反馈的部分说了什么", "trigger": "knowledge_retrieval", "params": {"query": "上周周报 客户反馈", "top_k": 3} } ] }注意几个细节:query直接让模型提取原话关键词就可以,不要让它过度改写;top_k给了上限,防止模型一口气要100个片段把上下文灌爆。
第二个表格查询技能。这个技能的关键是“自然语言到查询条件”的转换。
{ "name": "table_data_query", "description": "当用户需要查询结构化数据、统计数字、各渠道指标、用户增长情况等时使用。支持按渠道、日期范围过滤。仅处理表格类数据,不处理文档全文。", "parameters": { "metrics": { "type": "array", "required": true, "description": "要查询的指标列表,从用户消息中识别,如['新增用户','活跃用户']", "items": {"type": "string", "enum": ["新增用户", "活跃用户", "留存率", "付费用户", "收入"]} }, "date_from": {"type": "string", "required": false, "description": "起始日期,YYYY-MM-DD,用户未指定且需要本月数据时默认本月1号"}, "date_to": {"type": "string", "required": false, "description": "结束日期,YYYY-MM-DD,默认今天"}, "channel": {"type": "string", "required": false, "description": "渠道维度,如'all'表示不区分渠道"} } }这里要特别提醒:metrics用了枚举,把指标名称限定在系统可查询的范围内。我之前没加枚举,模型经常查询“销售额”这种系统里根本不存在的字段,导致执行函数每次都报错,用户体验极差。
第三个定时提醒技能。这个技能的难点有两个:时间的自然语言解析,以及和现有日历/任务系统的对接。
{ "name": "schedule_reminder", "description": "当用户要求设置提醒、定时任务、闹钟,或表达'到了某时间要做什么'时使用。将自然语言时间转换为cron表达式并注册。当用户只是在讲未来计划但并无明确提醒意图时,不要使用。", "parameters": { "reminder_text": {"type": "string", "required": true, "description": "提醒事项内容,从用户消息中提取"}, "cron_expression": {"type": "string", "required": true, "description": "cron表达式,根据用户提到的具体时间生成,例如'0 9 * * 1'表示每周一9点"}, "channel": {"type": "string", "required": false, "enum": ["app_push", "email", "sms", "im"], "description": "提醒渠道,默认app_push"} } }cron表达式是一个典型的“模型容易出错但可以靠规则修正”的参数。我建议不要直接让模型把cron字符串传给底层,最好在执行函数里再对cron做一次校验,或者用一个时间解释器把用户的“下周一早上9点”先转成标准化时间再转cron。这个后面讲坑的时候细说。
3.3 注册与编排:Agent主循环怎么把这些技能串起来
技能定义好了,代码里需要一个注册表。我用字典结构存储,把名称映射到执行函数和参数Schema:
SKILL_REGISTRY = { "knowledge_retrieval": { "handler": retrieve_knowledge, "schema": knowledge_schema }, "table_data_query": { "handler": query_table_data, "schema": table_schema }, "schedule_reminder": { "handler": register_reminder, "schema": reminder_schema } }Agent的主循环大致是这样:
def agent_loop(user_input): # 1. 用模型判断意图并选择技能 skill_name, params = model.decide_skill(user_input, skill_descriptions) # 2. 根据名称找到执行函数 if skill_name not in SKILL_REGISTRY: return "抱歉,我没有找到合适的能力来处理" handler = SKILL_REGISTRY[skill_name]["handler"] # 3. 调用执行函数,捕获异常 try: result = handler(**params) except Exception as e: return format_error_to_user(e) # 4. 格式化返回结果 return format_result(result)这套代码看着简单,但核心在decide_skill这一步。这一步不是简单的字符串匹配,而是把技能描述列表发给模型,让模型做“阅读理解”。所以我把技能描述写得那么详细,就是为这一步服务的。
3.4 上下文管理:技能返回的内容必须精简
技能执行完,返回给模型的数据会重新进入上下文,成为对话历史的一部分。这里有个容易被忽略的大坑:返回数据太大,会把上下文撑爆,后续所有能力都会退化。
我做过一个失败案例:文档检索技能返回了3万字原文给模型,结果模型在整场对话里都在复述这3万字,什么有效的事情都干不了。这就像找了一个资料给你,你光顾着抄全文,根本没时间提炼重点。
解决方法是定义三层返回:
- 摘要层:模型只拿摘要,大概200字以内。
- 结构层:必要字段的列表或表格,比如文档search结果的标题+时间+关键数字。
- 全文层:不返回给模型,存到内存或临时存储里,模型需要时按需读取。
返回体里message字段尽量写成“已查到3条相关内容,其中最相关的是XXXX”,让模型直接能照着这个话说给用户听。这样既控制了下游偏差,也提升了回复速度。
4. 实测中踩过的坑和排查清单
4.1 模型死活不调用技能的排查顺序
刚上手的时候,团队最常问我的问题就是“为什么我定义了技能,模型就是不调?”我自己的排查顺序是固定的:
第一步:看description有没有触发词。把用户那句话原封不动放进去比对,看描述里有没有对应触发点。比如用户说“查一下销售”,你的描述里如果没有“查”“销售”“数据”这类词,模型当然不会触发。
第二步:看参数是不是太难抽。如果技能本身没问题,模型也触发了,但参数抽不出来,模型会放弃调用。检查必填参数是不是太多,能不能加默认值。
第三步:看有没有配few-shot示例。这是准确率提升最大的杠杆。我见过一些项目,给技能加一个示例,触发率直接从40%干到80%。
第四步:看注册表有没有拼错。技能名称在描述里和注册表里不一致,这种低级错误我犯过不止一次。建议代码里加一个检查,启动时把所有技能名称过一遍。
还有一个容易忽略的点:模型连续调用技能失败两次之后,会开始胡言乱语,甚至编造调用结果。我在系统里加了硬性限制,同一个技能连续失败两次就停止调用,转为向用户解释,防止循环卡死。
4.2 技能执行报错后千万别把堆栈丢给用户
技能执行过程中的错误是必然的,关键在于怎么兜底。我把错误分成三类:
参数缺漏型:模型抽的参数不全。这类错误要回到更参数,比如给一个默认值重试。
上游接口异常型:比如下游服务超时、返回非200。这类错误要做重试,通常重试一次就好。如果重试还失败,直接给模型返回“服务暂时不可用”,让模型转述给用户,而不是把原始的500错误堆栈交给用户。
业务规则冲突型:比如查询时间范围超过系统最大支持范围。这种要返回可读的错误描述,并给出修改建议。
我统一封装了一个错误处理中间件,最后落到用户侧的格式永远是:“抱歉,我暂时没能完成XX操作,原因是系统XXXX,你可以尝试YYYY方式”。堆栈、请求ID、内部错误码,全部只留在日志里。
4.3 上下文污染的实战教训
这是我从实际运行中体会最深的一条。技能返回的数据,哪怕只有一次超出了限制,比如单次返回了3000字以上,整场对话质量就会断崖式下降。
为什么?因为模型是基于所有历史上下文来推理的,一次超大的无用数据混进去,后面的每一次推理都要带着这堆噪声。就像你的办公桌上被塞了一叠无关资料,之后每次找笔都要先把它扒拉开。
我的处理办法很简单:技能返回的内容在进模型上下文之前,先过一个“精简管”。管子里只放三样东西:
- 最终结论(一两句话)
- 必要的数据点(不超过5个数值或条目)
- 提示下一步动作(一句话)
其余的所有细节,都塞到另一个“附件存储”里。用户想看细节时,再通过后续技能调用去取。这套机制上线后,长对话的稳定性肉眼可见地提升了。
4.4 多技能冲突:描述边界重叠时模型会随机选择
技能数量一多,第二个问题来了:两个技能的描述边界重叠。比如文档检索技能和表格查询技能,用户问“上个月的报告里写的新用户数是多少”,两个技能都觉得自己应该上。
结果就是模型随机选一个,选了文档检索可能查不到具体数字,选了表格查询可能读不到报告里的上下文。这种随机性非常难排查,因为同样的输入,上次走的是A技能,这次走的是B技能。
解法有两个思路。一个是在description里加冲突消解说明,比如在表格查询技能里写“当内容出现在文档或报告中时,优先使用文档检索技能”。另一个更稳妥的做法是拆分触发条件,让两个技能严格按数据类型和场景走:一个只处理“文档里有什么结论”,一个只处理“系统里有什么数字”。尽量从源头避免重叠。
群里有个经验是“技能描述就是需求文档,多花一小时写清楚,后面省十小时debug”。我深以为然。
最后再分享一个小技巧
日志系统里一定要记“模型为什么选了这个技能”,不能只记“选了哪个技能”。我给每个技能调用都打了日志,记录的是模型决策时的完整上下文快照:用户原话、模型给技能的描述复现、抽出来的参数、以及置信度。这个日志在排查“为什么技能选错”时是救命稻草,没有它,所有问题都得靠猜。
另一个小建议:技能体系不是一锤子买卖。模型在升级、业务在变、用户的话术在变,技能的description要定期回看。我习惯每个迭代末把最近一周的误触发case翻一遍,挑出频次最高的几个,针对性更新描述。第一次做这个动作的时候,光是把两个重叠技能的边界重新划清,就让我们整体技能触发准确率涨了近20个点。
Agent技能这件事,说难不难,说简单也不简单,核心就是把模型的“聪明”焊接到真实的业务动作上。数据在流转、动作要闭环、错误要兜底,这套体系跑顺了,你手上的大模型才真正算得上是一个“能用”的系统。