ARTICLE DETAIL

资讯详情

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

Agent Skills 设计实战:从 Prompt 到可复用智能体技能体系

Agent Skills 设计实战:从 Prompt 到可复用智能体技能体系 最近一直在做 LLM 应用的人应该都绕不开一个词agent-skills。这词儿这两年确实火但怎么理解它差别很大。有人把它当成“给 Agent 多写几个 Prompt”有人把它当成“让模型多调用几次工具”。我的理解不太一样——它是把大模型的能力从临时发挥变成标准动作把一次次碰运气变成一套可复用、可组合、可验证的执行单元。今天这篇不聊概念就聊聊我自己从零梳理这套 Skills 体系的全过程踩过的坑、沉淀的经验、完整的实现思路都给整理出来。如果你正在做 Agent 类应用或者你的项目已经陷入“加 Prompt 一时爽迭代起来火葬场”的节奏这篇文章应该能给你一些提示。我会尽量把话说得实在点结构放得松一点讲讲为什么这么设计以及真实使用中会遇到哪些问题。1. 项目全貌agent-skills 是什么以及它解决的真实痛点先明确一下我口中的agent-skills指的是把 LLM 驱动智能体的某项能力封装成一套具备“输入假设、执行步骤、校验条件、失败兜底、产出去向”的独立技能模块。你可以把它想象成给海量的模型能力做“工种划分”——不是给模型塞一张嘴让它随便说而是给它一本岗位手册让它知道这个场景下该按什么流程干活。为什么需要一个“技能体系”最直接的刺激来自我自己的项目经历。我在做一批文档处理 Agent 时前期为了赶功能所有逻辑都写在那一大段 System Prompt 里。写的时候觉得挺顺的——效果也还行但迭代两版之后问题就冒出来了需求来了改动牵一发动全身改一处逻辑另一个场景的表现反而变差了模型升级一次没人敢保证原来的效果还好不好遇到某个边界情况你甚至说不清是模型理解错了指令还是流程本身有歧义。这些问题的本质不是 Prompt 写得不够好而是没有对行为做工程化分解。你让模型自己即兴发挥结果自然就飘忽。agent-skills 做的事情就是用显式的流程定义和可复用的执行单元把这种飘忽压下来。1.1 为什么 Agent 需要一套“技能”体系而不是更多提示词用一个生活里的类比讲讲这个区别。你找人来家里修水管。一种方式是你告诉他“你看着办弄好就行”他可能很有经验、可能发挥超常、也可能临时找不到工具然后浪费两小时。另一种方式是你给他一套 SOP进门检查水压、确认漏水位置、关总阀、拆接头、换垫圈、恢复水压测试每步都有检查项。第二种方式哪怕换一个从来没修过你家管子的人来只要他按步骤操作结果基本也可控。LLM 就是那个能力很强但状态不稳的维修师傅。Skills 就是那份 SOP。Prompt 是口头叮嘱Skills 是标准化流程。两者的差别有三个方面显式契约Prompt 只描述目标技能描述还要写清楚输入数据长什么样、输出物是什么格式、哪些情况下算完成、哪些情况要提前终止。结构化判断一个技能可以包含多段推理和多个工具调用不是“一句话让模型去做”而是“拆分步骤、逐段校验”。复用与组合Prompt 是点状的用完即弃技能是模块化的今天抽取出来明天可以组合进另一个更复杂的技能里后天还可以给别的 Agent 复用。所以我不建议再花时间堆 Prompt 了。想办法把日常需求里面稳定出现的那些“行为片段”抽成 Skills才是 Agent 工程化的正确角度。1.2 agent-skills 解决的任务迭代与可组合问题这个思路在我们项目里的收益非常明显。我回看过一期项目的改造过程当时我们有一个数据采集 Agent原本靠一段超长 Prompt 控制给定一批待处理的网页链接要求模型解析链接、抓取信息、清洗并输出结构化 JSON。第一版好不容易跑通了但每次接到新的采集源都要把那段 Prompt 翻出来不断加规则。加到最后Prompt 超过两千字模型开始“选择性遗忘”前面的一些约束偶发抽风有时候链接没抓全、有时候字段规范又出错。后来我痛定思痛把整个采集流程拆成了几个独立 Skills链接质量预检 Skill负责识别的链接模式、批量检测可达性页面正文抽取 Skill负责去除噪声、抽取主文本字段结构化 Skill负责把文本转换成目标 JSON 结构异常记录 Skill负责把无法处理的样本统一入库关键在于每个 Skill 都可以单独测试、单独回归验证甚至单独替换实现版本。新加一种采集规则只改“字段结构化”那个技能页面前置的处理根本不动。组合的价值也在这时候体现出来。我们后来要做一个“竞品更新提醒”功能本来以为要新写整个流程后来发现只要把两个已有的 Skills 串起来——数据采集那个连上“变更检测技能”——就基本完成了 80% 的工作。如果一个 Agent 平台只有无限叠加的 Prompt做这种组合几乎是重新再来但有了一层技能层很多新需求本质上只是“已维护技能的重新装配”。2. 技能工程的核心设计从指令到可执行协议的迁移看到这里你会发现我说的 Skills不是挂在 Prompt 里的“请你先想再写”而是真正可以被解析、被调度、被执行的可执行协议。这一节讲清楚我是怎么设计的。2.1 技能定义中不能忽略的三层结构我在设计自己的技能框架时参考了工业界对工作流和函数式编程的做法最后确定了一套三层结构分为决策层、执行层、校验层。决策层负责判断“当前这个请求适不适合用本技能处理”。比如用户的问题是“查天气”那你就不应该调用“代码审查”的技能用户的问题是“帮我给项目做一轮全面检查”那“代码审查”技能就需要被选中。决策层一般由模型承担但判断标准得清晰写出来。执行层负责按步骤调用外部工具或计算资源。这里强调的是步骤的确定性。把“怎么执行”写清楚模型只需要按顺序走中间遇到岔路时再根据条件选择分支。校验层负责在技能执行完之后检查产物是否合格。比如解析出的 JSON 格式是否有效、文件是否成功写入、关键字段是否为空。校验层是很多人忽略的但它是保证技能稳定的核心。这三层不一定要画成复杂的状态机写成一个结构化的技能描述文件就行。我常用的是 YAML 格式简单清爽。2.2 技能描述模板实例下面放一个我项目里实际在用的技能描述模板行为就是在给定目录下做文件清理归档。别被格式吓到每个字段我都会解释。meta: name: directory_cleanup description: 清理指定目录下的冗余文件并按规则归档保留文件 version: 1.2.0 input_schema: required: - target_dir - archive_dir optional: - days_threshold: 7 - min_file_size_mb: 10 decision_rules: - 如果 target_dir 不存在直接返回错误不执行后续动作 - 如果处理后没有发现任何需要清理的文件返回 no_action steps: - step_id: scan_files type: tool_call tool: fs_scan params: path: {target_dir} recursive: true - step_id: classify_files type: llm_judgement prompt: 根据文件清单将文件分为 keep 与 cleanup 两类。 保留条件最近修改时间在 {days_threshold} 天内或文件名包含 report、conf、template 等关键标识。 其他一律标记为 cleanup。 - step_id: do_cleanup type: tool_call tool: fs_move params: source_list: {cleanup_files} dest_dir: {archive_dir} validation_rules: - archive_dir 中必须存在刚才移动的文件 - target_dir 中不允许残留 cleanup 标记文件 failback: - 语法检查失败的路径全部放回原目录 - 如移动过程出现权限错误记录错误日志并继续处理下一个文件这个模板把 Agent 的“意识流”彻底落地成了工程单元。模型不需要自己发挥太多它只需要在classify_files这个步骤里发挥它的语义判断能力。扫描文件、移动文件这种高度确定的行为一律交给固定工具完成。整体下来稳定性高了不止一个档次而且每一步都能被审计、回溯和复现。2.3 运行时调度和模型之间的配合关系有了技能描述Agent 系统运行时就变成了一个“调度器”。我看过很多人把模型当作执行一切的引擎其实更合理的架构是模型当决策者技能当执行者校验器当监督者。实际跑的时候调度流程基本是这样的用户请求进入系统抽取意图并匹配技能列表找到最合适的 Skill。加载技能描述把输入参数填充进去。循环执行 Steps 列表每一步先判断当前 step 的类型是tool_call还是llm_judgement。执行工具时直接调用函数执行判断时调用模型并传入明确的判断指令。所有 Steps 执行完后运行校验规则。校验不过就进入回退逻辑或者重新执行相关步骤。这个架构最舒服的地方是模型被限制在它真正擅长的事情上语义理解与判断。至于精确计算、文件操作、网络请求、数据格式转换这些容易出错的部分都用固定代码完成。这样做之后我基本不会再被“模型算错了时间差、模型把文件路径写错了”这种低级错误困扰。3. 实操落地从零实现一个文件收集技能概念讲太多容易飘直接上一个能跑的实例。我会带着你们走完一个实际技能的完整落地过程收集指定目录内最近 7 天修改、体积大于 10MB 的文件按类型归档到另一个目录。这个技能很实用比如你可以拿它来做日志归档、设计稿收集、报告整理。整个过程不依赖任何重型框架核心逻辑也不复杂。3.1 定义技能语义与决策边界动手写代码前先把技能边界想清楚。这一步的目的是让模型明确“自己到底在干什么”避免后面产生幻觉式行为。我最初用一句话定义这个技能扫描目标目录找出符合条件的文件移动到归档目录并输出一张归档清单。然后列了几条决策规则目录不存在或目录下没有文件时属于no_action直接返回空清单。文件正在被占用打不开时跳过该文件但要在最终结果里标记为skip_locked。目标目录和归档目录相同属于非法配置直接报配置错误。这几条规则一旦固定下来模型的选择空間就清晰了调试的时候也特别好排查——看到返回skip_locked就知道不是模型抽风而是真碰到文件锁了。3.2 编写技能描述文件按照前面说的模板这个技能可以写成这样。我先把关键部分贴出来meta: name: collect_recent_files description: 收集指定目录下最近修改的大文件并归档到目标目录 input_schema: required: - source_dir - archive_dir optional: - days: 7 - size_mb: 10 decision_rules: - source_dir 不存在 → 返回空结果并提示 error - source_dir 与 archive_dir 相同 → 返回配置错误 steps: - step_id: scan type: tool_call tool: fs_scan params: path: {source_dir} - step_id: filter type: llm_judgement prompt: 在下面文件列表中筛选满足两个条件的文件 1) 最近修改时间在 {days} 天内 2) 文件大小不小于 {size_mb}MB。 注意文件夹本身不要输出无法访问的文件单列到 skipped 列表。 - step_id: copy_and_archive type: tool_call tool: fs_copy_move params: source_files: {filtered_files} dest_dir: {archive_dir} mode: move validation_rules: - filtered_files 中的每个文件都存在于 archive_dir 中或者出现在 skipped 列表 - 最终返回 JSON 必须包含三个字段archived、skipped、no_action failback: - 移动中出现 IOError 的文件记录到 skipped 中不中断流程这个文件同时也在扮演“生成最终回复的格式契约”因为最后 validation 明确要求返回 JSON三个字段清清楚楚。3.3 后端执行模块与模型判断的衔接有了技能描述执行框架通常只需要一个通用循环就够了。为了便于理解我用 Python 写一个极简伪代码风格的中枢逻辑def run_skill(skill_yaml: dict, inputs: dict) - SkillResult: ctx load_context(skill_yaml, inputs) validate_input(ctx) # 校验入参和决策规则 for step in ctx.steps: if step.type tool_call: result execute_tool(step.tool, fill_params(step.params, ctx)) elif step.type llm_judgement: result llm_judge(step.prompt, ctx.partial_result) else: raise UnsupportedStepType(step.type) ctx.partial_result[step.step_id] result errors run_validation(ctx, skill_yaml.validation_rules) if errors: run_failback(ctx, skill_yaml.failback) return build_result(ctx)代码里最核心的就三件事填充参数、按顺序执行、校验结果。整套逻辑复用性极强你新增一个技能基本只需要新增 YAML 文件和配套的工具函数中枢不用大改。这也是技能化改造后代码迅速变清爽的根本原因。关键衔接点在于filter这个 llm_judgement 步骤。我传进模型的不是文件内容而是前面扫描出来的文件清单。模型只负责分类判断不要让它去读文件、统计大小这些工具能做得又快又准。我实际跑下来模型判断出错率大幅下降。它不需要理解“10MB 等于多少字节”这种计算问题只需要理解“哪个文件看起来像报告哪个像缓存”。3.4 测试用例设计和验收经验技能做完不能凭感觉说能用我习惯直接拿测试用例跑。下面这张表是我用过的典型用例覆盖了正常路径、异常路径和边界场景。用例编号输入目录状态预期行为T01目录里有 5 个符合条件的文件5 个全部移动JSON 的 archived 长度为 5T02目录为空目录返回no_action: true不执行任何移动T03文件被其他进程锁定该文件进入skipped不影响其他文件移动T04source_dir 不存在返回 error_code不执行后续步骤T05文件名重复且跨目录按扫描路径区分移动后归档目录自动加后缀T05 是我踩过最尴尬的坑。一开始移动文件直接shutil.move两个不同来源目录里同名文件直接互相覆盖最后狼狈地找备份。后来我在copy_move工具函数里加了重名自动加时间戳的逻辑才算彻底解决。这类问题通常不在 LLM 的考虑范围内所以更应该在工具函数层面兜底。4. 踩坑实录Skill 化改造中的常见问题与排查这条道路我也不是一次跑通踩过不少坑。挑几个最典型的出来说说都是你没跑过几轮很难注意到的细节。4.1 技能描述太长导致模型理解漂移第一次写技能模板时有点“处女座”怕描述不仔细往 YAML 里塞了很多补充说明、边界示例、历史背景。结果运行后发现模型对decision_rules判断老跑偏甚至把一些示例当成了硬性规则。症状技能的决策边界时灵时不灵某些示例被当成固定模板导致输入变化时表现僵化。原因描述文件过长有效信息被稀释。LLM 对中间位置的注意力本来就弱写过细反而提高关键信息的丢失率。处理办法把描述文件控制在 300 行以内只写“必需”和“高频异常”两类信息。其余细节尽量下沉到工具函数层面去兜底不要用自然语言去约束模型做太多防御式理解。我自己的经验是能用代码兜底就少用 Prompt 交代。比如“文件不存在”这种判断天然应该由代码返回异常而不是让模型先把心提到嗓子眼。把注意力留给真正需要语义判断的步骤。4.2 校验规则写太松问题被后置暴露另一个极端是校验规则不够严格。早期我只校验了“最终 JSON 是否包含三个字段”没校验“archived 里的文件是否真的都存在”。结果有一次归档目录写错程序返回正常但文件移到了错误目录等发现已过了好几天。症状Skill 执行返回成功但产物不符合实际预期。原因validation_rules 定义不完整只校验收据结构没校验系统真实状态。处理办法校验不仅要检查模型输出还要检查外部工具的可观察结果。比如移动文件后立刻检查目标目录文件是否存在写数据库后立刻回查该条记录。从那以后我给自己定了个规矩校验规则至少覆盖一层“对宿主系统真实状态的验证”不能只停留在返回值判断上。4.3 技能之间参数命名混乱导致组合失败做技能组合时技能 A 的输出参数叫file_list技能 B 的输入参数叫source_files中枢系统拼接的时候没有做映射结果 B 技能跑起来全是空。症状两个单独都能用的技能组合起来就失效。原因技能之间没有约定统一的公共数据模型。处理办法在设计技能描述时我把公共字段规范成了一套标准名词文件列表统一叫file_manifest归档目录统一叫archive_dir异常记录统一叫error_log。这有点像接口约定一旦约定好了组合时只需要做轻微适配甚至免适配。这个坑尤其提醒我Skills 不是孤岛它们的输入端与输出端迟早要对齐。在命名上吃亏后期组合成本成倍增加。4.4 模型“幻觉”卡在 llm_judgement 步有一类问题让很多人疑惑明明我给 llm_judgement 的 prompt 很简单直接为什么模型还是会输出一些不存在的文件路径我排查下来的主要原因是信息拼写错误源头不是模型“笨”而是模型在感知节点上出现格式错觉。比如文件清单里某路径特别长模型在回复时自己缩短/省略了一部分路径。所以处理方式是在执行层做静态校验llm_judgement 返回的文件路径必须与 scan 输出的原始路径逐一比对发现不匹配就拒绝。让模型尽量返回索引比如第几号文件而不是直接返回完整路径字符串路径映射由代码完成。这一招实测非常稳。模型其实不擅长“抄写长字符串”让它做判断把精确抄写交给代码可以彻底消灭大部分路径幻觉问题。5. 进阶思路技能编排、复用与演化如果你已经成功把几个核心行为抽成 Skills接下来最值得关注的就是编排与演化。这一节说说我在长期使用中体会到的东西。5.1 一个多技能协作的小案例举个例子我想做一个“定时整理桌面并把整理日志发给团队群”的小应用。听起来要写不少逻辑但实际拆解后就是三个技能的组合collect_recent_files扫描桌面收集最近修改文件并移入归档目录generate_changelog读取归档目录生成一份人类可读的 ChangeLogsend_message把 ChangeLog 发送到指定 Webhook组合编排不算复杂关键在于技能之间的参数传递。我的编排脚本长这样collected run_skill(collect_recent_files, { source_dir: desktop_path, archive_dir: archive_path, }) if collected.no_action: logging.info(nothing to collect, skip) exit(0) log_text run_skill(generate_changelog, { file_manifest: collected.archived, format: markdown, }) run_skill(send_message, { webhook_url: hook, content: log_text, })三个技能各自独立维护哪怕其中一个升级另外两个也不需要感知。这种低耦合的特性是我做这套架构后觉得最舒服的地方。5.2 演化方向技能注册、索引与自动复用多技能协作变多后我意识到下一个阶段的问题是技能发现。当技能数量超过几十个时决策层如何在一大堆技能里选出正确的那个这里面有两条路一条是建技能索引库每个技能维护好语义标签调度时走语义检索匹配。这适合技能数量中等、类型不太复杂的场景。另一条是给技能维护依赖图和调用优先级让决策模型基于任务描述做多层推理选择。这个适合复杂场景但对工程能力要求更高。我目前是两者结合着用。技能索引负责召回决策模型负责精排实测之后选中的准确率在可控范围内。将来的技能市场也是类似思路发布者给技能写好描述和输入输出契约使用者按语义搜索组合——这本质上和现在做的这套技能体系是一脉相承的。5.3 沉淀技能的时机与意识最后给一个很多人问的“什么时候该抽技能”的建议。我的判断标准只有一个同一个行为模式是否已经出现第三次重复使用。第一次出现直接写 Prompt快速验证。第二次出现观察它和第一次的不同点临时适配。第三次出现果断抽成 Skill并且设计好通用输入输出。这么做的好处是你不会一开始就过度抽象也不会等系统“乱成一锅粥”才想着收拢。技能抽取是渐进式的是对现实需求的反应而不是憋一个大而全的框架。拿我自己的项目来说很多东西都是在重复到第三轮的时候才抽成技能每次抽取带来的收益都立竿见影——因为已经被现实验证过了不必凭空猜需求。6. 写在最后的实操体会做了一段时日之后如果让我总结 agent-skills 这套体系最重要的东西我可能会说它不是给模型加一个外挂而是给自己加一个“工程边界”。你花时间定义输入输出、校验规则、失败回退本质上是在告诉系统行为边界在哪里。我实测下来最大的感触是自从把提示词改造成技能之后排查问题的效率提升非常明显。以前是“这段题词改一下试试”现在是直接看审计日志和校验规则问题定位耗时长已经是分钟级别。这种稳定感对于做 To B 场景的用户太重要了。如果你现在手头有一个 Prompt 已经不受控的 Agent 项目我的建议是别着急推翻重来。先从经常做、代价高、一错就怕的那件事入手抽成一个独立 Skill跑通流程再逐步覆盖。用不了太久你也会发现这套技能化做法的甜头。
返回列表