ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

agent-skills实战:从提示词到技能编排,构建可靠的AI Agent工作流

2026/9/23 5:35:32 拓冰建站 浏览量
agent-skills实战:从提示词到技能编排,构建可靠的AI Agent工作流 先聊个真实的场景。我最近在做一个内部工具需要让大模型根据用户的一句话自动完成“查资料-汇总要点-生成表格-发送到群里”这一整条流程。第一版我图省事把所有指令全写在系统提示词里结果模型经常记不住步骤偶尔还自己发挥把格式弄得乱七八糟。后来我把这套流程拆成了几个独立的“技能”让模型在合适的时机主动调用整个项目瞬间就稳了。这就是“agent-skills”想解决的核心问题别再指望把一切都塞进上下文而是把能力拆成模块让智能体按需使用。这篇内容适合正在做Agent应用、或者想优化现有大模型工作流的人。无论你是刚接触LangChain这类框架的初学者还是已经在生产环境里调教过模型的老手把“技能”这个概念理清楚都能让你的系统少踩一半的坑。我会从概念讲起逐步拆解设计思路再用一个完整案例带你把技能跑通最后分享一些我实测过的高频问题和避坑经验。1. agent-skills是什么先弄清楚概念再动手1.1 技能不是提示词也不是插件很多人第一次接触agent-skills会把它和“提示词模板”或者“插件”搞混我一开始也是。它们的区别其实很本质提示词是在教模型“怎么说”技能则在定义“能做什么”。你可以把技能理解为一种“能力封装”。它包含了三个东西一份给模型看的描述一段实际执行的代码或接口以及一个明确的数据输入输出协议。模型看到描述判断这个技能适不适合当前任务然后带着参数去调用它。整个过程就像你在公司里有个“财务报销”的流程——你不用自己懂税法、懂发票规范只需要提交一份符合格式的申请单财务部门会按流程帮你办完。技能就是那个“财务部门”。插件则是更偏向工具层的概念往往直接暴露一堆API给模型。技能和插件可以共存但技能的粒度通常更接近“能完成一个任务目标”而不是“能调一个接口”。比如一个“发送邮件”的技能内部可能调用了认证、内容格式化、SMTP发送好几个接口但对模型来说它只需要表达“我想发一封邮件给某某”剩下的全藏在技能内部。明白这个区别之后你就知道为什么“agent-skills”是当前大模型应用从“能聊天”走向“能干活”的关键一步了。1.2 为什么现在的Agent应用都要聊技能我观察到一个现象今年年初还能看到不少团队在尝试“纯提示词驱动的Agent”到了年中基本销声匿迹了。原因很简单真实业务场景里的操作太多、太碎而且要求可重复、可验证。大模型天生擅长的是语义理解和生成并不擅长精确执行带状态的流程。举个例子你让模型“帮我把这个Excel里的数据按日期排序然后生成一个柱状图再写一段分析结论”。纯提示词方案里模型可能得靠猜测来“想象”Excel内容然后给出一步步操作建议但最终执行还是得靠人。而技能方案里“排序并提取数据”是一个Python脚本技能“生成图表”是一个图表库技能“写分析结论”才是模型真正该做的事。每一步都是可执行、可测试、可回滚的。这就引出了agent-skills的两个核心价值可靠性和可观测性。可靠性来自“把模型擅长的事留给模型把模型不擅长的事交给代码”可观测性来自“每个技能都有明确的输入输出出了问题能快速定位是哪个环节挂了”。后面章节我会详细展开这两个价值在实际项目里怎么体现。2. 核心设计拆解一个技能系统由哪几部分组成2.1 技能注册表给所有技能一个“档案”想管理好一堆技能第一件事是给它们建一个注册表。你可以把它理解成一套索引记录每个技能的唯一ID、名称、描述、参数结构、入口函数和返回格式。注册表的作用不只是给模型看的也是给开发者和运维者看的。我建议注册表用JSON或YAML这类结构化格式来维护而不是硬编码在代码里。这么做有几个好处第一技能信息可以动态加载新加一个技能只需要改配置文件不用重新发版第二配置和代码分离后非开发人员也能参与技能描述的人工校对第三方便做自动化测试注册表本身就是一份完整的测试清单。实际项目里一个技能的注册信息大致长这样{ skill_id: fetch_webpage, name: 抓取网页正文, description: 当用户需要获取某个URL页面的正文内容时使用返回纯文本格式的页面正文。, parameters: { type: object, properties: { url: { type: string, description: 需要抓取的完整网页地址 } }, required: [url] }, entry: skills/fetch_webpage.py, output_schema: { type: object, properties: { title: { type: string }, content: { type: string }, length: { type: integer } } } }你可能会问这个注册表是给谁看的答案是两方都要看。一方面系统要把注册表里的“描述”和“参数”部分定期喂给大模型让它知道有哪些技能可用另一方面实际调度器要依据“entry”字段去加载对应的执行函数。一个字段都不能省尤其是description和output_schema这两个直接决定模型能不能正确选技能、能不能正确解析结果。2.2 技能描述决定大模型会不会用你的技能在我的经验里技能系统有没有效果一半取决于代码写得好不好另一半取决于描述写得好不好。模型的工具调用能力再强如果技能描述含糊不清它照样会选错或用错。写技能描述有一条核心原则站在模型的角度写而不是站在开发者角度写。不要写“该函数用于获取HTTP响应”而要写“当用户提到查询或访问某个网址时使用本技能获取网页正文”。模型在处理用户请求时是靠语义匹配来判断技能的所以描述里一定要包含“触发场景”和“大致效果”。再给几个我实测有效的细节。第一描述里要写明“不适用于”什么场景能有效减少误调用。比如“本技能仅适用于公开网页不适用于需要登录的页面”。第二参数描述里注明单位、格式和限制比如“时间格式为YYYY-MM-DD早于2000年的日期不支持”。第三如果某个技能需要特定的前置条件必须写清楚比如“调用本技能前请先调用get_access_token获取访问令牌”。有时我会给同一个技能写两个版本的描述一个精简版本用于初筛一个详细版本用于模型需要进一步了解时再提供。这种方式能省不少token同时还能保证模型在复杂场景下有足够信息做判断。2.3 调度执行从意图到动作的关键一跳注册表和描述解决了“模型知不知道该用”的问题接下来就是“怎么用”的环节也就是调度执行。这一层主要做三件事解析模型返回的调用请求、校验参数、执行实际函数并返回格式化结果。调度器的设计有个容易被忽视的重点不要完全信任模型给出的参数。模型偶尔会凭空捏造参数值尤其是当它漏读了上下文信息时。我在调度器里加了一个参数校验层使用注册表里的parameters定义做格式校验必要时还会对枚举值、取值范围做显式检查。如果校验失败调度器会返回一个“参数错误”的信息给模型并附上正确的参数定义让模型有机会自我纠正。这个设计每次都能挽救不少对话轮次。执行完之后调度器还要负责统一结果格式。我一般会把执行结果包成一个标准结构包含status成功或失败、data实际内容、error_msg错误消息。这样可以保证无论技能内部是调用API、操作数据库还是读取文件模型看到的返回结构都保持一致降低后半段的解析成本。3. 实操过程把第一个技能完整跑通3.1 场景定义以“查询航班信息并生成出行建议”为例概念说多了容易飘不如直接上个例子。我选一个很常见的应用场景用户说“帮我查下后天从北京到上海的航班我想选早班机然后给我一个住宿建议”。这个需求拆解下来其实涉及两个技能一个是“查询航班”一个是“查询目的地酒店”最后还要模型基于两个结果生成一段综合建议。你看一个看似简单的请求其实已经是技能组合了。我们先不管组合先从单一技能“查询航班”说起。我设想这个项目里航班数据来源于一个内部模拟接口返回JSON格式的航班列表包含航班号、起降时间、价格、余票等信息。为了演示方便我先把技能做成直接调用这个接口不去接真实的航司API这样你复现起来没有环境门槛。3.2 技能定义与执行逻辑实现按照前面注册表的结构我先定义一个精简版的skill配置文件存成flight_search.json{ skill_id: search_flights, name: 查询航班信息, description: 当用户需要查询某两个城市之间、某个日期前后的航班时使用。可指定最早起飞时间、最晚起飞时间等筛选条件。, parameters: { type: object, properties: { origin: { type: string, description: 出发城市如北京 }, destination: { type: string, description: 到达城市如上海 }, date: { type: string, description: 出发日期格式YYYY-MM-DD }, earliest_time: { type: string, description: 最早起飞时间格式HH:MM可选 } }, required: [origin, destination, date] }, entry: skills/search_flights.py, output_schema: { type: array, items: { type: object, properties: { flight_no: { type: string }, departure_time: { type: string }, arrival_time: { type: string }, price: { type: number }, remaining_tickets: { type: integer } } } } }然后是实现文件search_flights.py核心逻辑如下import json from datetime import datetime def search_flights(origin: str, destination: str, date: str, earliest_time: str 00:00) - dict: # 模拟数据真实项目里这里会去请求航班查询API mock_data [ {flight_no: CA1501, departure: 07:30, arrival: 09:50, price: 1200, remaining: 5}, {flight_no: MU5102, departure: 08:15, arrival: 10:30, price: 980, remaining: 12}, {flight_no: HO1252, departure: 09:00, arrival: 11:20, price: 1050, remaining: 0}, {flight_no: CZ8888, departure: 13:00, arrival: 15:15, price: 760, remaining: 20} ] filtered [] for item in mock_data: if item[departure] earliest_time: filtered.append(item) if not filtered: return {status: success, data: [], error_msg: None} return {status: success, data: filtered, error_msg: None}这里有几个细节值得说。第一执行函数没有直接返回原始mock数据而是包了一层status和error_msg就是为了统一返回结构方便调度器解析。第二函数参数名和注册表parameters里的属性名保持一致减少调度时做名称映射的工作量。第三剩下票数为0的记录仍然返回给模型因为模型需要知道这个航班存在但没票了这会影响它的推荐策略。3.3 调度器接入大模型让模型学会调用技能代码写好了配置也写好了接下来最关键的一步是把技能信息同步给大模型。目前主流的方式是把技能描述注入到系统提示词里或者通过结构化工具调用的方式传给模型。以我用的框架为例它会读取注册表把技能列表转成模型工具定义格式然后每次对话时自动携带。假设我们用的是OpenAI的function calling格式转化出来的工具定义长这样{ type: function, function: { name: search_flights, description: 当用户需要查询某两个城市之间、某个日期前后的航班时使用。可指定最早起飞时间、最晚起飞时间等筛选条件。, parameters: { type: object, properties: { origin: {type: string, description: 出发城市如北京}, destination: {type: string, description: 到达城市如上海}, date: {type: string, description: 出发日期格式YYYY-MM-DD}, earliest_time: {type: string, description: 最早起飞时间格式HH:MM可选} }, required: [origin, destination, date] } } }当模型识别到用户意图需要查航班时它会返回一个工具调用的请求包含函数名和参数。调度器收到后从注册表找到对应的entry执行search_flights函数再把返回结果注入到对话上下文里让模型基于真实数据生成最终回答。我实际跑这一套流程时第一个版本踩了个小坑模型在解析“后天”这个相对日期时偶尔会算错。后来我在描述里加了一条要求“date参数必须使用工具当前日期推算”并在系统提示词里注入当天日期问题才彻底解决。这说明技能描述的边界条件必须根据真实失败案例持续迭代不是写一次就完事的。4. 技能组合与多Agent编排进阶玩法4.1 串行、并行、条件分支编排的三种基础模式单技能跑通只是起点大多数真实需求都涉及多个技能的协作。我总结了一下技能编排主要有三种基础模式串行、并行和条件分支。串行最简单一个技能的输出恰好是另一个技能的输入。比如“查询航班”得到航班列表再把航班号传给“获取航班详细动态”这就是串行。实现上只需要把前一个技能的data字段取出填充到后一个技能的参数里。并行用于几个技能之间互不依赖的场景。比如用户问“北京和上海明天天气怎么样”系统可以同时调用两个“查天气”技能只是参数不同。并行能显著减少等待时间但要注意别把并行的任务数量设置太大我一般控制在3个以内否则模型容易在汇总时遗漏部分结果。条件分支则是最能体现“智能”的模式。模型先调用一个“判断意图”的技能根据返回结果决定走哪条分支。比如用户说“如果上海下雨帮我改成高铁出行”系统就得先查天气再基于结果决定是否调用“查高铁”技能。条件分支的实现要点是分支判断逻辑尽量写在代码里不要靠模型反复推理因为模型在连续多轮决策中稳定性会下降。4.2 技能之间的依赖与数据传递多技能协作时最头疼的问题永远是数据传递。A技能的输出字段名是priceB技能需要的字段名是amount模型又不会自动做字段映射最后经常导致调用失败。我的做法是在注册表里增加一个“output_mapping”字段显式声明技能输出和其他技能参数的对应关系。比如B技能需要amount而A技能输出里有price那我就在编排配置里写清楚“amount A.price”调度器在执行序列时自动做映射。另外一个更隐蔽的问题是上下文截断。当多个技能的返回结果都塞回对话历史很快token就会爆炸尤其是那些返回大段文本的技能。我的经验是对于中间步骤的技能输出不需要把完整内容全部返回给模型只返回关键摘要即可。比如“抓取网页正文”技能返回了5000字的内容调度器可以先用另一个技能或正则规则提取前500字作为摘要再注入对话。这样既保留信息又控制成本。4.3 多Agent协作时技能怎么分配再往上一层就是多个Agent协作的场景。比如一个Agent负责理解用户需求另一个Agent负责调用外部工具还有一个Agent负责审核生成结果。每个Agent都可以拥有自己的技能子集这种隔离在团队协作和安全控制上很有意义。我这边实践下来的建议是先定义清楚每个Agent的角色边界再分配技能顺序不能反。如果角色边界模糊就会出现两个Agent抢着调用同一个技能的情况浪费token不说还可能产生重复操作。比如“用户需求分析师”这个Agent应该只具备“意图识别”和“信息检索”技能不具备“发送通知”这类执行类技能“执行Agent”则相反不配置太多分析类技能。每个Agent的技能子集小一点模型的选择难度就低一点准确率也会明显提升。我在一个项目里把单一Agent的8个技能拆给三个Agent整体任务成功率从73%提到了91%。这个提升幅度应该能说明问题。5. 常见问题与避坑经验5.1 高频问题排查速查表把这段时间我在agent-skills项目里遇到的问题整理成了一张表。这些问题都很典型如果你也在做类似的事情大概率会碰到其中几个。问题现象根本原因排查方法解决建议模型从不调用某个技能技能描述里的触发词和用户表达差异太大查看模型日志里的工具选择记录重写描述加入更多同义表达模型调用了技能但参数缺失必要的参数没有放在required里检查工具定义中的parameters结构把必填参数加入required数组技能执行成功但结果错误函数内部逻辑有bug或数据源返回格式变化单独测试入口函数绕过模型为技能编写单元测试数据源变更时能快速发现模型在工具结果出来后无法正确总结返回结果太长模型丢失关键信息查看模型完整输入序列在结果注入前做摘要或结构化预处理系统提示词太长请求经常超时技能数量太多所有描述全量注入统计token消耗改为动态加载不匹配当前上下文的技能先不注入同一个技能被重复调用模型没看到前一次调用结果或者上下文被截断检查多轮对话的状态管理加强上下文管理确保前一轮工具结果稳定保留这张表看起来简单每一条背后都是我至少踩过两三次才总结出来的经验。尤其是“技能执行成功但结果错误”这一条我一度以为问题出在模型上查了两天才发现是上游接口的字段名变了被我的异常捕获逻辑悄悄吞掉了。现在我在每个技能入口都加了原始返回的日志排查问题的效率高了不是一点半点。5.2 技能设计的几个关键细节与经验思考技能不是越多越好。我见过一些项目恨不得给模型开几十个技能结果模型在选择时经常犹豫不决甚至选错。我的经验是如果某个技能在最近100次对话里被调用的次数为0就应该考虑把它下掉或者合并到其他技能里。技能越少模型的选择越准确响应速度也越快。技能粒度要适中。太小的技能会让单次任务拆出很多步骤增加调用开销和失败概率太大的技能又会导致复用一个技能完成不同的语义任务让模型对输入参数的理解产生混乱。我一般把握的尺度是一个技能应该对应“一次可以独立验收的任务”比如查天气、查航班、生成图表、发送邮件而不是“HTTP GET请求”这种原子操作。另一个容易被忽略的细节是错误处理策略。技能执行必然会有失败比如网络超时、数据为空、权限不足。调度器要提前定义好失败后怎么办是直接返回错误给模型重新想办法还是重试一次还是换备选技能。我的默认策略是“重试一次-失败后把错误信息返回给模型”并明确在提示词里告诉模型“如果工具执行失败不要编造数据要如实告知用户”。防止模型在技能失败后脑补不存在的执行结果这一点非常重要。5.3 从一个基础框架开始的清单建议最后分享一个我用来评估技能系统是否健康的自检清单。每次上线新技能或调整技能时我都会拿它过一遍实测对整个系统的稳定性帮助很大。技能描述里是否写清楚了触发场景和排除场景必填参数是否都已标注并做了校验技能的返回结构是否和注册表里的output_schema一致技能执行失败时是否有明确的错误信息返回给模型这个技能的调用频率是否值得保留如果技能依赖外部API超时和限流处理是否已覆盖是否记录了完整的调用日志包括输入参数、原始返回和最终结果这七条看起来都是基础工作但真正做到位的项目并不多。尤其是日志记录很多团队在起步阶段嫌麻烦等出了问题才发现无从排查。我现在要求所有技能入口都打印“输入参数”和“耗时”两项成本极低收益却不小。我在实际项目中最后悔的一件事就是在初期没有重视技能的版本管理。技能更新时直接覆盖了旧代码结果线上模型和数据源不兼容排查了半天才发现是新旧版本的接口参数变了。后来我养成了一个习惯每个技能的代码、配置、描述必须同步进版本库并且技能变更后要跑一遍回归测试确认模型在“技能调用选择”环节没有被新的描述影响。这套流程跑顺之后整个agent-skills体系才算真正进入一个稳定迭代的节奏。希望这篇内容能让你少踩一些我踩过的坑尽快把你自己的技能库跑起来。