
从去年开始我一直在带团队做企业级智能体Agent项目最常被问到的问题不是“模型选哪个”而是“为什么我的Agent跑着跑着就失控了”。明明Demo阶段效果惊艳一上真实场景就开始胡说八道、工具调用错乱、该用A技能的时候偏偏去调B技能。调试到最后你会发现问题往往不出在模型推理上而是出在技能体系的设计上。这篇文章想聊的就是围绕“agent-skills”这个核心如何从零到一构建一套让Agent稳定落地的技能体系包括技能的定义、沉淀、编排、评估和维护。内容基于我实际项目中的经验总结适合正在做Agent应用、被“能跑但不好用”困扰的开发者参考。1. 为什么Agent项目跑着跑着就失控了——技能体系缺失的典型症状很多人把“技能”简单理解为“给Agent接个工具”。这是最大的误解。工具只是接口技能是“工具触发条件输入输出规范执行逻辑”的完整封装。没有技能体系的Agent就像一个人空有一身肌肉但没有神经反射每个动作都要临时思考一遍怎么做。1.1 失控的三个典型场景我在复盘多个项目后整理了下面这些高频失控场景。如果你也在做Agent开发看看是否似曾相识场景一技能边界模糊导致串任务。你给Agent配了一个“查天气”的API和“查航班”的API两个接口参数结构相似结果Agent经常把航班号当成城市名去调天气接口。这不是模型笨是技能描述里没写清楚“这个技能的适用边界是什么、什么情况下不应该用”。场景二同一任务行为不可复现。用户问同样的问题Agent十次回答中有七次走A流程、三次走B流程。原因在于你没有把“完成这个任务的标准步骤”固化下来每次都在靠Prompt临时编排。别指望大模型每次都能自行想出最优路径它确实能但稳定性完全不可控。场景三新需求只能靠改Prompt硬撑。业务方提了个新需求你不想新写代码就往系统Prompt里加一段“如果用户提到XX就调用XX接口”。Prompt越来越长最终Agent开始忽略后面的指令或者把矛盾指令混在一起执行。这三个场景的根因是同一个Agent缺少一套稳定的、可复用的技能层来承载业务逻辑。我们做项目时形成的共识是——大模型负责“理解”技能体系负责“执行”。理解可以发散但执行必须收敛。技能体系就是那层把发散的理解映射到确定性执行的“骨架”。1.2 技能体系要解决的核心问题明确了症状再来看技能体系的职责清单。一套合格的技能体系至少要覆盖四个核心问题是什么技能做什么事输入输出是什么依赖哪些工具或数据源。何时用什么场景触发这个技能什么场景禁止使用如何判定触发条件。怎么做技能内部的执行步骤是什么每一步的判定标准和异常处理路径。如何验证技能跑得好不好误用率高不高怎么评估和迭代。说白了技能体系就是给Agent装了一套“肌肉记忆”。好比你开车新手要默念“踩离合、挂挡、松手刹”老司机根本不过脑子肌肉自动完成。Agent有了技能体系遇到已覆盖的任务时就不需要临时推理直接按固化流程执行遇到没覆盖的任务时才动用大模型的推理能力去临场发挥。这套架构下Agent既保持了泛化能力又获得了确定性。2. 技能的定义与表示从Prompt片段到结构化技能描述想清楚技能体系要解决什么问题之后第一件事就是定义技能的表示格式。这一节我会给出一个经过多个项目验证的技能描述结构并解释每个字段为什么要这样设计。2.1 一份完整的技能描述长什么样我把技能定义为一个JSON文档字段不多但每个都有明确用途。以下是一个“物流时效查询”技能的完整示例{ skill_id: logistics_eta_query, version: 3.2.0, name: 物流时效查询, description: 根据快递单号和承运商查询包裹预计送达时间。适用于用户询问包裹何时送达、是否延误、当前运输进度等场景。当用户仅提供运单号且未指明承运商时需要先从运单号规则推断承运商无法推断时先询问用户。, tags: [物流, 快递, 时效, 查询], trigger_conditions: { required: [用户明确表达了查询包裹送达时间的意图], forbidden: [用户仅询问运费价格, 用户想修改收货地址], confidence_threshold: 0.85 }, input_schema: { properties: { tracking_number: {type: string, description: 快递运单号}, carrier: {type: string, enum: [sf, zt, yt], description: 承运商代码} }, required_properties: [tracking_number], optional_properties: [carrier] }, execution_flow: [ {step: carrier_inference, description: 如果carrier为空根据运单号前缀规则推断承运商推断不出来则反问用户}, {step: api_call, description: 调用物流查询API携带tracking_number和carrier}, {step: result_parse, description: 解析API响应提取预计送达时间、当前位置、异常状态如滞留、退件}, {step: response_generate, description: 按模板生成回复先答核心结论送达时间再补充说明当前位置与异常情况} ], error_handling: [ {condition: API返回运单号不存在, action: 回复未查询到信息并建议用户核对单号}, {condition: API响应超时, action: 回复临时故障引导用户稍后再查}, {condition: 承运商不在支持列表, action: 回复暂不支持该承运商建议用户去对应官网查询} ], metrics: { success_rate: 0.94, false_trigger_rate: 0.02, avg_execution_time_ms: 380 } }2.2 关键字段的设计意图description字段是重中之重。很多团队在技能描述上偷懒写一句“查询物流时效”就完了。可实际上大模型做技能路由时靠的就是这段描述来判断“现在该不该用这个技能”。你的描述越模糊模型就越容易在边界场景上犯迷糊。写技能描述有一个原则我称之为“给一个聪明但没见过世面的实习生看”。你招了个名校实习生聪明是真的聪明但不懂你的行业黑话、不清楚你的业务边界。你要让他能准确判断“这个活儿归你管”就得把职责范围、工作边界、歧义处理都交代清楚。技能描述同理。上面那个示例中我特意加了一条“当用户仅提供运单号且未指明承运商时需要先从运单号规则推断承运商”——这就是给实习生的明确指令避免他拿着空carrier直接去调接口。trigger_conditions字段解决“何时用”的问题。required和forbidden是两个容易被忽略的高级设计。required定义这个技能启动的必要条件forbidden定义禁止场景优先级更高的条件。比如用户说“帮我查一下这个包裹还要多久到顺便告诉我运费多少钱”——主意图是查时效但顺带问了运费。如果没有forbidden条件Agent可能直接唤醒“运费查询”技能导致主意图被干扰。在这里我会把“仅询问运费价格”放进forbidden确保Agent优先执行主任务。confidence_threshold字段也值得说道。这是技能路由时的判定阈值。0.85这个数值不是拍脑袋定的是我们统计了6000条真实对话后调出来的。阈值太高Agent会倾向于“不知道用什么技能”导致大量任务落到兜底逻辑阈值太低则容易误触发。我建议每个团队至少积累几百条真实日志后再去调这个参数初期设0.8上下后面根据误触发率回修。execution_flow字段定义了“怎么做”。把执行步骤拆成有顺序的单元目的不只是告诉模型先干嘛后干嘛更重要的是切出“可观测的中间状态”。万一某一步出错了你能精准定位是单号推断的错、API调用的错、还是结果解析的错而不是对着一个黑盒干瞪眼。这其实是在为后面的可观测性和评估打基础。error_handling字段是很容易被漏掉的。没有异常处理路径的技能遇到API报错就只能把原始错误信息甩给用户。一套成熟的技能必须预判“哪些错误是高频的、哪些需要引导用户换一种方式”。这一块写得好能直接决定用户体感——处理“查不到”这个场景时你是说“抱歉查不到”还是“建议核对单号后重试”完全不是一个技术水平的产品。3. 技能的两种来源从零编写与从对话中沉淀技能定义好格式之后下一个问题是技能从哪来我做过不少项目总结下来技能来源主要有一条“手工路线”和一条“半自动路线”两者缺一不可。3.1 从零编写一套可复用的拆解方法手工写技能需要方法不然很容易写成“大号流水账”。我常用的拆解框架是先梳理Agent需要覆盖的任务清单然后把任务分成三类信息问答类。用户问什么Agent答什么核心是检索和归纳。比如“查一下今天股价”、“这个产品的退换货政策是什么”。这类技能的关键是知识来源的可靠性和答案格式的稳定性。任务执行类。用户给出目标Agent调度工具完成任务。比如“帮我订一张明天去北京的高铁票”、“把这份合同转成PDF发给张总”。这类技能的关键是步骤编排和异常回滚。分析建议类。用户抛出问题Agent给出判断。比如“帮我评估这两个供应商哪家更合适”、“这份账单里有哪些异常开销”。这类技能的关键是分析框架的固化和输出结构的稳定。拆出这三类任务清单后再对每个任务写JSON技能描述。我的建议是不要贪多先挑出项目里最高频的20到30个场景来做。在第一个真实项目中我们把Agent能干的活全列出来之后发现真正被反复调用的技能只有核心的几个剩下的长尾场景完全可以交给Prompt泛化处理。技能体系的建设是动态迭代的过程一开始追求全覆盖会把自己累死。3.2 从对话日志中沉淀半自动的工作流纯手工编写技能有一个天花板你永远只能写下你预见过的问题。真实世界里用户的问题千奇百怪大量技能点藏在历史对话里没被发掘。所以我们需要第二条路线——从对话日志中反向沉淀技能。我在项目中落地过一个半自动沉淀流程操作步骤如下第一步采集Agent的历史对话日志筛选出“调用工具成功”和“调用工具失败但用户最终满意”两类样本——前者说明现有流程可行后者说明Agent可能自创了一条可复用的路径。第二步对样本做意图聚类。可以用embedding把用户的query转成向量再做聚类。不需要特别复杂的语义模型直接用text-embedding类接口就行。第三步观察每个聚类簇的主题看簇内样本是否代表一类频繁出现的任务诉求。如果一个簇的样本量超过阈值我一般定在50条/周就值得沉淀为一个新技能。第四步参考聚类簇内成功执行的对话轨迹反向补全技能描述里的execution_flow和error_handling——为什么这一步走通了走通的路径是什么遇到报错时是怎么兜底的这套半自动流程的核心思路是把优秀Agent的临场发挥变成所有Agent的标准能力。某个Agent在特定场景下自己摸索出了一条很好的执行路径这套路径被日志记录下来沉淀成技能之后所有Agent都能直接复用。这相当于把一次偶然的成功变成了制度化的成功。3.3 一个简单的聚类脚本思路这里我贴一个极度简化的代码思路做日志聚类的骨架方便参考import json from sentence_transformers import SentenceTransformer from sklearn.cluster import KMeans # 加载历史对话日志 with open(conversation_logs.json, r) as f: logs json.load(f) # 提取用户query queries [log[user_query] for log in logs if log.get(tool_called) or log.get(success)] # 生成embedding model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) embeddings model.encode(queries) # 聚类 k 12 clusters KMeans(n_clustersk, random_state42).fit_predict(embeddings) # 输出每个簇的部分样本人工判断是否有沉淀技能的价值 for cluster_id in range(k): sample_indices [i for i in range(len(queries)) if clusters[i] cluster_id] labels [queries[i] for i in sample_indices[:10]] print(f簇 {cluster_id} 样本量 {len(sample_indices)}) for label in labels: print(f - {label})在这个思路上你可以继续扩展加上时间窗口过滤、加上规则去重、甚至用大模型给每个簇自动打标签。我在内部跑通这套流程之后每周能从日志里稳定沉淀出3到8个新技能点比纯手工梳理效率高了一个数量级。4. 技能编排的关键让Agent学会判断“什么时候用什么技能”技能建好了下一个核心问题就是编排。一套技能库里有几十个技能Agent怎么知道这个用户问题该用哪个技能这正是“agent-skills”中最关键、也最容易被做砸的部分。4.1 技能路由从模糊匹配到结构化决策很多团队做技能路由就是一股脑把所以技能描述拼到系统Prompt里让模型自己选。这个方案在小规模技能库下也许能跑通一旦技能超过20个性能就会快速退化。Prompt太长导致注意力分散相似的技能描述互相干扰加之各路技能描述质量参差不齐引发严重的误触发。我在项目里用的是“结构化路由预案”的组合方案。当用户问题进来时先生成用户意图的向量表示然后去技能库中做语义检索召回Top K个候选技能K一般是5到8再让大模型从候选技能中做最终选择。这样做的好处是大模型不用从几十个选项里盲选只需从召回的少数几个中做筛选准确率会高很多。召回方案我有两种实践路径。轻量级做法是把技能描述跟用户query一起做embedding计算余弦相似度取Top K重量级做法是引入一套独立的rerank模型先做粗召回再用交叉编码器精排。项目初期不需要上重量级方案余弦相似度加规则修正已经能跑出不错的基线。import numpy as np from sentence_transformers import SentenceTransformer model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) # 预先对全部技能描述生成embedding skill_embeddings {skill_id: model.encode(skill[description]) for skill_id, skill in skills.items()} def route(user_query, top_k5): query_vec model.encode(user_query) scores { skill_id: compute_similarity(query_vec, skill_vec) for skill_id, skill_vec in skill_embeddings.items() } # 先按相似度取TopK候选 candidates sorted(scores, keyscores.get, reverseTrue)[:top_k] return candidates召回完成之后把候选技能的完整JSON描述喂给大模型做最终裁决大模型输出“该用哪个技能、理由是什么、需要填哪些参数”。在进入大模型裁决前还可以加一个硬规则过滤层——比如用户直接说“退换货”那就不需要召回后裁决直接按“售后处理”技能执行。这类高频场景用规则兜底能显著降低延迟和犯错率。4.2 技能编排的两层逻辑谈到“编排”大家很容易想到复杂的工作流引擎。但我实际落地时会区分两层逻辑单一技能的内部编排和多技能的动态组合。单一技能的内部编排在技能的execution_flow里已经写好了属于确定性流程适合固定执行。这里更大的挑战在于多技能动态组合一个用户问题可能涉及多个技能。比如“把这份报表发给财务并总结关键数据”听起来与报告生成有关与发送消息有关很依赖触发间隔和前置条件。这种场景靠单技能路由是搞不定的。我用的方案是“技能链skill chain”模式定义技能之间的依赖关系和输出传递协议。前一个技能执行完成后的结构化输出作为后一个技能的输入上下文。比如chain: - skill: report_generation output_key: report_summary - skill: message_send input_from: report_summary recipient: finance_group这个YAML描述的就是一条技能链先调用报告生成技能整理数据和摘要把摘要作为下一环的输入参数交给消息发送技能推送到财务群里。技能链定义好之后大模型的任务就是选择“该走哪条链”而不是“从头到尾即兴编排”。这样设计有一个额外的好处技能链是可视化的、可调试的。一条链出了问题你很快能定位是哪个环节断了。如果全靠模型即兴编排出了问题你只能翻对话记录逐段推理——监控成本极高。4.3 技能数量膨胀后的路由退化陷阱技能库从20个涨到50个的时候路由召回精确率会明显下降。这不是模型退化了而是技能描述之间的相似度不可避免地在增加。我处理这个问题时有两个经验。第一个经验给技能加标签维度不要把描述当成唯一的检索信号。我在技能JSON里加了tags字段每个技能打3到5个业务标签召回时同时匹配描述和标签标签权重可以调高。这样即便描述相似度高标签差异也能帮助区分。第二个经验高频技能做独立的硬规则入口。项目里调用量最高的前5个技能直接写规则匹配不参与模型路由。比如用户提到“查物流”正则和关键词直接命中“物流查询”技能根本不需要走检索逻辑。把高频确定性路径从模型决策里拿掉路由压力骤减整个系统的稳定性能上一个台阶。5. 实战对比同一个任务有无技能体系的效果差异理论讲了这么多我们来做一个直观对比。假设现在有个任务用户要求Agent去对比两个开源项目的License类型和社区维护活跃度。我会用两种方案执行方案A是纯提示词加工具调用方案B是技能体系路径。差异会非常明显。5.1 方案A纯提示词实现这是很多初版Agent的真实形态系统Prompt大概长这样你是一个智能助手。你可以调用以下工具 - github_search(repo_name)搜索GitHub仓库信息 - license_lookup(license_text)解析License文本 - commit_activity(repo_name)查询仓库近三个月commit记录 当用户让你对比两个仓库时依次获取两个仓库的License类型、最近提交频率然后给出结论。看起来逻辑没问题但实际跑起来你会在两个地方吃亏。第一当用户问法偏离预设时执行路径立刻涣散。用户如果直接问“这俩项目哪个更活跃”Agent可能只调commit_activity工具完全不查License信息尽管用户前面说过要“对比License和活跃度”。因为在纯Prompt模式下Agent每次都是临时规划路径一旦复述问题不完整规划路径就会跟着遗漏需求点。第二工具选择容易踩坑。如果一次要用多个工具而工具返回数据的结构很相似Agent可能会出现把commit数量当成License类型字段来用的错乱。不是大模型能力不行而是你没有给它足够的结构化约束。5.2 方案B技能体系实现同一个任务我们把它沉淀成一个“仓库对比分析”技能{ skill_id: repo_compare_analysis, description: 对比两个开源仓库的License合规性和维护活跃度输出结构化对比表格。适用于用户要求对比仓库选择、评估开源项目可用性等场景。, trigger_conditions: { required: [用户明确提出了对比两个或多个开源仓库], forbidden: [用户只是在单点查询某个仓库的信息] }, execution_flow: [ {step: repo_info_fetch, description: 分别调用工具获取两个仓库的元信息包括License类型、Star数、最近更新时间}, {step: license_parse, description: 解析License文本判断是否为宽松许可MIT/Apache-2.0还是强copyleft许可GPL标记合规风险等级}, {step: activity_check, description: 查询两个仓库近90天的commit频率和issue响应时间形成活跃度评分}, {step: compare_output, description: 生成结构化对比表分License维度、维护活跃度维度给出结论和建议} ], error_handling: [ {condition: 某个仓库无法访问, action: 单独标注该仓库信息获取失败不阻塞另一个仓库的分析}, {condition: License类型未知, action: 标记为未知并提示用户自行核实不强行猜测} ] }这时Agent不用“临场发挥”直接走既定流程该查的字段一个不少该避开的坑全部提前规避。5.3 效果差异核心指标对比我拿同一个任务在同等模型条件下跑了20次统计了几个关键指标指标方案A纯Prompt方案B技能体系任务完成率70%100%License与活跃度双维度覆盖14/20次20/20次工具字段错用次数4次0次平均响应时间7.2秒5.8秒输出格式一致性低每次排版不同高同一模板方案B在质量、稳定性和速度上全面胜出。其中“平均响应时间”变短这一点很值得解释一下看似技能体系多了几步流程校验应该更慢才对但真实原因是执行路径固定后减少了无效推理和接口错调重试总耗时反而降了。模型在流式推理时省去了“我该怎么编排”的思考开销直接按执行流走每一步都是确定调用。5.4 这个差距背后的机制根源我不认为这是“技能体系有魔法”本质上就是把不确定性决策前移到了设计期。纯Prompt方案把所有决策压力都留在推理期让模型每次都在高维空间里临时搜索路径而技能体系在编译期就把最优路径固化成了可复用的资产。你可以把技能体系理解成“给大模型装了一份精装修的地图”它不需要自己探路只需要按导航走遇到新路段再临时开荒。这套架构下稳定性和效率的提升是自然结果。6. 技能维护的持续性工作评估、版本管理与淘汰机制很多人在做完技能库、跑通编排之后就认为大功告成。这个想法会埋下隐患。技能体系与其他软件一样需要持续维护否则会随着业务演进而腐化。我最后重点聊聊维护阶段要做好的三件事评估、版本和淘汰。6.1 评估集构建三类case缺一不可靠感觉判断“这个技能好用不好用”是不行的。我们团队维护了一套技能评估集持续评估每个技能的健康度。评估集里必须至少包含三类case已知覆盖case。每个技能配20到30条它应该被正确触发的标准问题验证这个技能该出场时确实能出场执行流程没有跑偏。边界混淆case。每条边界case专门设计成“看着像别的技能但其实是这个技能”或反之。比如“物流时效查询”技能的边界case可以写“帮我查一下申通单号SF开头的是不是顺丰”——这个query里有“顺丰”字样但用户实际是要判断物流公司归属并查询时效。边界case是评估误触发率的关键素材。模型知识盲区case。这类case是专门测大模型“也不确定自己在说什么”的情况。比如一套法律合规规则很可能没有被训练语料完整覆盖。这类case不指望Agent完全答对而是观察它是否在准确告知能力边界和引导人工介入——这一点比答案本身重要。6.2 版本管理与回归测试技能描述本质上也是代码应当纳入版本管理。我们的做法是把每个技能保存为独立文件放回到代码仓库里每次改动都走Pull Request评审并附带变更说明。技能文件更新之后先在评估集上跑回归测试确认“老case的通过率没有下降新case的通过率达标”然后才发布上线。这里有一个细节值得注意技能描述是会被大模型反复读取和推理的文本你改一句话的连锁反应可能非常隐蔽。有一次我把某个技能的forbidden条件从“用户仅询问运费价格”改成“用户未提供订单号”改动当天核心链路指标下跌了两个百分点。排查半天才发现改完之后的forbidden条件把“用户既查时效又问运费”这个原本正确处理的高频case给过滤掉了导致这些请求全部落入兜底逻辑。从那以后我们的技能改动强制要求写明“影响范围预测”并回顾所有关联case。6.3 淘汰机制不要让技能库变成垃圾场没有淘汰机制的技能库会随着时间不断膨胀最终变成一个彼此干扰、检索退化的“垃圾场”。我建议定期做技能健康度审查主要看三个指标命中率过去30天该技能被路由选中并成功完成任务的次数/被路由选中的总次数。误用率触发后发现用户意图不匹配的占比。成功率技能执行完成后用户明确满意的占比。我的个人经验是一个技能一旦连续两周命中率低于1%且无上升趋势或者误用率超过15%就进入淘汰观察池。观察两周后如果还没有好转就降级为“非推荐技能”并从主路由中剔除避免它继续污染路由召回的质量。这样做看起来很冷酷但确实是维护技能库质量必要的取舍。6.4 技能健康度周报的模板分享一个我们团队在用的技能周报模板你可以直接复制改造技能ID调用次数成功率误用率平均耗时备注logistics_eta_query48294%2%380ms稳定repo_compare_analysis26100%0%1.2s新增持续观察refund_process1275%18%900ms建议优化触发条件每周花一小时扫一遍这个表技能库的状态就在掌控之中。技能体系不是“建完就完”的一次性工程而是需要持续浇灌的活系统。我在实际项目中最大的体会是技能体系的价值是复利式的。项目初期花在技能设计上的时间不会立刻兑现容易让人怀疑这笔投入值不值但坚持维护三到六个月后你会明显感觉到Agent的表现越来越稳定——这不是模型升级带来的而是你的技能资产在慢慢累积。技能库也是有生命的系统持续喂养它、修剪它它才能在你真正需要的时候派上大用场。