ARTICLE DETAIL

资讯详情

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

Agent技能工程实战:用Skills让LLM稳定执行任务

Agent技能工程实战:用Skills让LLM稳定执行任务 做智能体应用一年多了坦白说让LLM聊起来从来不是问题真正折磨人的是让它稳定把活干完。早期我的Agent一直处于一种每天都在打架的状态工具列表越长翻车概率越高Prompt越改越长最后连我自己都说不清它到底会怎么理解哪一段。直到我把整个体系按技能Skills的形态重构了一遍——也就是业界常说的 agent-skills 思路——情况才明显好转。我把这个过程中的核心落地方案整理出来围绕 agent-skills 讲清楚几个问题技能到底解决了什么一个合格技能长什么样我是怎么手写一个“会议纪要结构化”技能并挂进Agent的以及那些翻过车之后才总结出来的排查经验。适合正在做Agent应用、被多工具调度搞到焦头烂额、或者刚开始接触技能工程的开发者往下看。1. 先搞清楚一件事Agent Skills到底解决什么问题1.1 技能是什么把“会聊天”变成“会干活”的关键一层很多人理解Agent LLM 工具调用但实际落地时你会发现中间缺了一层东西。LLM强在推理和语言弱在没有稳定的“肌肉记忆”。人做PPT之前不需要重新学习什么是PPT因为那套流程已经内化成本能了LLM如果没有技能层每次都要在上下文里重新组织完整步骤稍有干扰就变形。技能就是把“完成某类任务的方法、步骤、输入输出契约、示例和注意事项”打包成一个完整单元。模型只做一次决策——这个任务需要调用哪个技能、输入什么、输出怎么接——剩下的执行交给确定性很强的代码。这层设计让LLM从“每次即兴发挥”变成“按套路出牌”稳定性的提升不是一点半点。我整理过一张对比表用来跟团队解释为什么非要做技能层不可维度全靠Prompt硬写纯Tool原语调度技能层Skills执行路径LLM每次现场生成LLM临场组合原语固定代码执行LLM只做路由稳定性低改一处崩全局中工具多了易选错高可测可回归调试成本高改Prompt影响全部任务中要查调用链路低问题收敛在技能内部可测试性基本不可测部分可测可以完整写单元测试复用性差全靠复制粘贴有但太碎强文档代码测试一体表格里“固定代码执行”这几个字是关键。Agent技能的运行不依赖模型在关键时刻“人品爆发”它把最需要稳定性的那部分从模型中剥离出来前置到代码层。1.2 为什么堆Prompt和叠Tool解决不了稳定性问题我见过两个极端。一种是什么都往系统Prompt里塞任务步骤、输出格式、业务规则、few-shot示例全堆进去。这种做法在单任务时看起来很爽但任务一多Prompt每一处微小改动都是全局性的很可能这个任务修好了另一个任务悄悄坏掉。更隐蔽的是写Prompt的人容易不自觉地把它当成“解释器指令”可LLM本质上不是线性的解释器它会自由发挥。另一种极端是把所有能力都做成Toolsend_email、query_db、search_web、generate_report……工具列表越来越长。Tool是原语级能力Agent需要自己编排调用序列工具越多选择空间就越大。我实测过工具数量超过15个之后模型选错工具的概率明显上升有时候它甚至把两个功能相似的Tool混着用输出驴唇不对马嘴。技能层刚好卡在中间。它把多个原语和固定步骤组合成一个高层操作比如一个“会议纪要结构化”技能内部可能要调用文本切分、LLM抽取、去重合并三个步骤但对Agent来说它就是一个名字为 meeting_minutes 的黑盒。15个工具压成5个技能选择熵大幅下降模型只需要在抽象层级上做决策做对的概率自然高得多。1.3 什么时候需要技能层三个信号如果你还不确定自己要不要上技能层我列几个信号满足任意一条都值得动手同一个流程你已经第三次用大段Prompt写了而且每次写都还要重新调。你发现输出格式怎么正则都修不稳每次都要额外写后处理代码去补漏。工具调用序列非常固定但Agent依然每次临场编排偶尔编排错。我自己的经验是只要任务边界清晰、有人反复在做它就应该被固化成一个技能。固化得越早后续调试成本越低。2. 拆解一个成熟技能SKILL.md、实现代码与测试三件套业界比较有代表性的技能工程方案是Antropic提出的“技能即目录”思路一个技能不是一个孤立函数而是 SKILL.md 实现代码 测试三件套的组合。我沿用了这个骨架稍微做了工程化扩展下面把每一块的作用讲清楚。2.1 SKILL.md写给模型看的说明书比代码注释重要一百倍先明确一件事模型不能直接执行你的代码它只能读文本做决策。所以技能的“路由说明书”必须是一个模型能读懂的Markdown文件。SKILL.md的质量直接决定模型何时调用、怎么调用、调用后能不能用对。我写SKILL.md有一套固定模板# meeting_minutes会议纪要结构化 将一段无序的会议转写文本整理成结构化会议纪要输出JSON。 ## 何时使用 - 用户提供了会议录音的文字转写、访谈记录要求整理纪要 - 用户说“帮我记一下会议结论”“整理行动项”“提炼待办” ## 何时不要使用 - 用户只是闲聊没有提供任何转写文本 - 用户要求调用日历、发送邮件那是其他技能的职责 ## 输入 - raw_text字符串必填原始转写文本 - meeting_date字符串可选格式YYYY-MM-DD ## 输出 严格JSON对象字段包括title、date、attendees、summary、decisions、action_items。 ## 示例 输入...一条真实转写片段 输出...对应JSON ## 注意事项 - 不要编造原文不存在的人和事 - 负责人不确定时填“待定”模板里每一块都有目的。“何时使用”和“何时不要使用”是给模型做路由判断的模型会根据用户输入和这里的描述做语义匹配触发描述写得越贴近真实用户口语路由越准。“示例”是给格式化做参考的模型会把示例当成输出模板一定要放一个完整且正确的例子。“注意事项”则是行为红线防止模型在自由生成的环节乱来。如果你发现技能经常不被调用八成是SKILL.md写得不清楚不是模型傻。2.2 实现代码纯函数优先少搞状态技能的实现代码我的原则是三个纯函数优先、无状态、异常信息可读。纯函数意味着输入输出都是JSON可序列化的数据不依赖全局变量和外部副作用。为什么因为技能会被Agent框架以各种方式调用可能有状态包装、可能有并发一个带着隐式状态的技能很难排查。把状态外置成参数调用方想传就传不传就走默认值。LLM调用建议隔离成一个单独的内部函数不要散落在主流程里。这样测试时可以轻松把LLM调用mock掉只测逻辑。另外错误消息一定要可读比如“模型输出中未找到JSON”和“字段attendees缺失”这类信息在技能重试时会被重新喂给模型写得清楚能帮模型自我纠正。2.3 测试给“不太可控”的LLM部分划一个可控边界有人跟我说Agent技能没法写测试因为LLM输出随机。这话只对了一半。LLM输出确实随机但技能的绝大多数代码逻辑是确定性的——分块、解析、校验、合并这些完全可以单测。真正随机的只有模型调用那一小步把它mock掉之后整个技能就是可测试的。我会写三类测试单元测试覆盖分块和合并逻辑集成测试mock掉LLM给定固定输入断言输出结构完整再加一组golden test把一组真实转写文本跑完后的结果存成期望快照技能升级时对比防止行为悄悄退化。技能迭代最怕的不是不进步而是改一个输出格式把之前能正确抽取行动项的能力给弄没了。2.4 一个技能一个目录文件怎么组织技能的目录结构我推荐这样skills/ meeting_minutes/ SKILL.md skill.py test_skill.py assets/ example_input.txt example_output.json为什么不用单文件因为技能会长大。SKILL.md是文档测试是保障代码是主体assets存放真实样例各归其位。一个目录就是一套完整的、可复制的能力单元团队协作时直接把整个目录丢给对方什么都不用解释。3. 实战手写一个“会议纪要结构化”技能从零到挂载3.1 需求拆解哪些任务适合做技能我每周有大量会议语音转写出文本原始转写用词口语化、逻辑跳跃、多人说话混在一起直接丢给模型让它“写个纪要”结果经常时好时坏。后来我决定把它做成一个正式技能。判断任务适不适合做技能我的标准是四连问边界清不清楚有没有高频重复需求输入输出能不能明确定义结果需不需要稳定结构会议纪要结构化完全满足这四条。反例是“陪用户聊产品方案”这类开放对话它更适合作为Agent的常规能力而不是技能——技能需要可验证的具体产出。这个技能的边界我一开始就划死了只做转写文本到结构化纪要的转换不负责自动发送邮件、不自动创建任务卡、不推进度。边界越清晰Agent编排越简单后续接其他技能也越顺。3.2 定义输入输出边界越清晰失误越少输入我把raw_text定为必填meeting_date和language定为可选。这个设计很朴素但关键是输出Schema它直接决定模型抽什么、怎么抽。我定义的JSON结构如下{ title: 会议主题, date: 2025-01-15, attendees: [张伟, 李娜], summary: 三句话概括会议内容, decisions: [决策1, 决策2], action_items: [ {owner: 张伟, task: 完成XX方案, due: 2025-01-20} ] }字段不多但每个都有讲究。action_items是结构化对象owner和due都允许为空字符串这比让模型硬编一个“未知”要稳定。decisions单独列出是因为会议纪要里“决定了什么”和“要做什么”经常被混在一句话里分两个数组能让后续自动化更好处理。输出必须是JSON而不是Markdown原因很实际JSON可以直接被下游流程消费也不用再去解析。3.3 核心实现分块、抽取、合并代码部分我直接给一个可运行的版本里面包含三个核心环节分块、LLM抽取、合并去重。import json import re from typing import Any, Callable, Optional # LLM 调用函数抽象。你需要传入一个签名为 # def llm_fn(system_prompt: str, user_text: str) - str # 的函数返回值是模型输出的文本且要求是 JSON 字符串。 CompletionFn Callable[[str, str], str] SYSTEM_PROMPT 你是一名会议纪要助理。你的任务是把用户提供的会议转写原始文本整理成结构化的 JSON。 要求 1. 输出必须是最外层合法的 JSON不要输出任何解释文字。 2. JSON 结构必须严格符合以下 schema { title: string会议主题10字以内, date: string会议日期YYYY-MM-DD, attendees: [string参与人姓名], summary: string3句话概括会议内容, decisions: [string会议明确做出的决策], action_items: [{owner: string负责人, task: string待办事项, due: string截止日期不知道就填空字符串}] } 3. 如果原文中没有明确说出某项的负责人owner 填待定。 4. 不要编造原文中不存在的信息。 def chunk_text(text: str, max_chars: int 12000) - list[str]: 按段落把长文本切成块尽量在语义完整处切断。 paragraphs [p.strip() for p in re.split(r\n\s*\n, text) if p.strip()] chunks: list[str] [] current for para in paragraphs: if len(current) len(para) 1 max_chars: chunks.append(current) current para else: current f{current}\n{para} if current else para if current: chunks.append(current) return chunks def _parse_json(raw: str) - dict: 从模型输出中提取 JSON兼容模型喜欢加代码块标记的情况。 start raw.find({) end raw.rfind(}) if start -1 or end -1 or end start: raise ValueError(f模型输出中未找到 JSON{raw[:200]}) return json.loads(raw[start:end 1]) def _extract_one(chunk: str, llm_fn: CompletionFn) - dict: raw llm_fn(SYSTEM_PROMPT, chunk) return _parse_json(raw) def merge_results(results: list[dict]) - dict: 合并多个分块的抽取结果按内容去重。 merged { title: results[0].get(title, ), date: results[0].get(date, ), attendees: [], summary: , decisions: [], action_items: [], } seen_people: set[str] set() seen_decisions: set[str] set() seen_tasks: set[str] set() summaries: list[str] [] for r in results: for att in r.get(attendees, []): if att and att not in seen_people: seen_people.add(att) merged[attendees].append(att) for dec in r.get(decisions, []): if dec and dec not in seen_decisions: seen_decisions.add(dec) merged[decisions].append(dec) for item in r.get(action_items, []): key (item.get(owner, ), item.get(task, )) if key not in seen_tasks: seen_tasks.add(key) merged[action_items].append(item) if r.get(summary): summaries.append(r[summary]) merged[summary] .join(summaries) return merged def run_meeting_minutes( raw_text: str, llm_fn: CompletionFn, meeting_date: Optional[str] None, max_chars: int 12000, ) - dict: 会议纪要结构化技能主入口。 chunks chunk_text(raw_text, max_chars) results [_extract_one(c, llm_fn) for c in chunks] merged merge_results(results) if meeting_date: merged[date] meeting_date return merged几个实机经验补充一下。分块时我按空行切段落再拼块这比按固定字符数硬切要稳得多模型不容易在半句话上断掉。块大小默认12000字这个数字要看你的上下文窗口别卡太满要给输出预留空间。_parse_json里先找第一个“{”再从后往前找最后一个“}”是因为很多模型输出会在JSON外面包一层json代码块标记直接json.loads必然失败。这个坑特别常见。merge_results的去重用的是(owner, task)二元组因为同一个负责人的同一件事在长会议上可能被重复提多次。summary我简单做了拼接实际生产我会改成语义合并但作为技能1.0拼接够用了。真实使用时llm_fn参数可以换成任何厂商SDK。比如用Anthropic SDK就是from anthropic import Anthropic client Anthropic() # API Key 通过环境变量 ANTHROPIC_API_KEY 提供 def anthropic_completion(system: str, user_text: str) - str: response client.messages.create( modelclaude-sonnet-4-5, # 换成你自己可用的模型ID max_tokens4096, systemsystem, messages[{role: user, content: user_text}], ) return response.content[0].text result run_meeting_minutes( raw_textlong_transcript, llm_fnanthropic_completion, meeting_date2025-01-15, ) print(json.dumps(result, ensure_asciiFalse, indent2))把LLM调用抽象成一个函数好处是明显的想换模型、想加缓存、想埋点都只改一个地方技能主体完全不用动。3.4 挂载进Agent给模型一份足够的调用指引技能写完还要让Agent知道它的存在。如果你的Agent框架支持动态技能加载通常只需要把技能目录注册进去框架会自动读SKILL.md并注入上下文。但不支持的话就需要在系统Prompt里手动加一段路由指引我习惯这样写你有以下技能可用 - meeting_minutes把会议转写文本整理成结构化会议纪要。 触发条件用户提供会议录音转写、访谈记录等原始文本并要求整理纪要时使用。 不要使用用户在闲聊、单纯询问建议时不要启动。 输出JSON对象。注意这里的关键词和SKILL.md保持一致。模型可能同时读到系统Prompt和技能文档两边描述冲突它会不知所措。保持一致不是可选项是必须项。3.5 验证结果定性不定量的检查清单挂载完成后我建议按这个清单验证技能是否真的“稳”同一份输入跑5次确认输出Schema完全一致。随机抽3条行动项对比原文确认没有编造内容。空字段检查负责人未知时是不是“待定”而不是瞎填。多块文本合并后有没有重复项残留。如果改了分块参数跑一遍既有测试组防止回归。这套验证不追求跑分追求的是“能放心让它自动干活”。技能的价值不在惊艳在可预测。4. 常见问题与排查技能不生效时先看这四个地方4.1 技能没被调用先查说明文档再查触发词最常遇到的问题就是技能写好了Agent死活不调用。我的排查路径是固定的。第一步确认技能确实已经加载很多框架加载的是旧副本清缓存再看。第二步打开Agent的推理trace观察模型在路由决策阶段有没有看到这个技能如果看到了但不选大概率是SKILL.md的触发描述和真实用户输入对不上。比如你写的是“会议纪要”用户说“帮我记一下刚才那半小时聊了点什么”模型可能不觉得是同一件事。解法是给SKILL.md补充口语化触发词和反例示例越贴近真实用户用语路由越准。这类问题不是模型问题是文档工程的细节问题。4.2 技能调用了但结果畸形输出schema校验和重试机制症状是解析失败、字段缺失、枚举值乱。根因通常是LLM没有严格遵守Schema——小模型尤其明显。我给的方案是三层防护。第一层解析后做Schema校验缺什么字段直接抛错。第二层做小规模重试把错误信息原样回喂给模型让它自己修最多重试两次避免死循环。第三层在System Prompt里用“输出必须是最外层合法的JSON”这类负面约束把格式问题前置拦截。动手改之前先跑一次调用看是模型抽不动还是格式乱两个问题改法完全不同。4.3 长文本处理翻车分块策略和上下文窗口的关系分块会让模型丢失跨块上下文。比如某段发言说“刚才张伟提的那个方案我同意”但张伟的方案内容在上一块。我的对策是重叠分块每块尾部带上一块末尾200到500字符这样关键指代词不会悬空。生产环境里我还会把第一块抽出的attendees列表作为额外上下文传给后面的块实体就一直在视野内。还有一点合并重复项时action_items的“负责人任务”二元组去重只能处理完全相同的文本语义重复处理不了。这个问题目前没有完全自动的办法我在技能里加了一个review步骤让模型在合并后做一次轻量去重。4.4 多技能互相干扰命名空间和加载顺序的教训技能多了以后新的问题会出现技能A不触发技能B反而抢着触发。我踩过最狠的一个坑是两个技能描述里都用了“总结”这个词模型经常把需要完整会议纪要的请求路由到简洁摘要技能上。解法有三个。第一技能命名带领域前缀例如meeting_minutes、report_writer、data_cleaner不要用summarize、process这种过于通用的名字。第二按任务域分组加载用户在做会议相关操作时只加载会议域技能别把所有技能全塞给模型。第三高优先级技能排在描述列表前面模型对列表前部的内容敏感度更高这个我实测属实。4.5 避坑清单速查表症状可能根因排查顺序推荐对策技能完全没被调用SKILL.md触发描述与真实输入不匹配1.看trace路由日志 2.检查描述用词补充口语化触发示例和反例调用后输出仍然畸形Schema约束不严或模型抽取能力不足1.单测解析逻辑 2.观察原始输出加校验与重试考虑换更强模型长文本结果遗漏后半段分块后上下文断裂1.对比单块结果 2.检查merge逻辑重叠分块透传实体摘要多个技能互相抢调用描述宽泛、命名冲突1.检查各技能描述 2.看加载列表加前缀按域分组加载改动某技能导致整体行为变化改了SKILL.md影响路由1.对比版本diff 2.跑golden test变更前记录技能行为快照这张表基本覆盖了技能工程落地期能遇到的80%问题剩下的都是具体业务逻辑问题靠日志就能定位。5. 从单技能到技能系统编排、复用与演进5.1 复杂任务拆解把“写周报”拆成三个技能单技能解决单任务组合技能才能解决复杂工作流。以“写周报”为例我拆成三个技能log_fetcher负责从项目系统拉取本周提交记录meeting_minutes负责整理本周会议纪要report_writer把前两者输出汇总成结构化周报。三个技能各自独立、各自可测组合起来就是一个完整的周报流水线。拆分的判断标准是“变化频率”。如果三个环节经常单独变化——比如提交记录的数据源变了、会议纪要的格式升级了那它们就应该拆开。如果周报整体很少变化也可以合成一个大技能少做一次编排。拆还是不拆看技术债的积累速度不要为了拆而拆。5.2 技能组合的两种编排模式串行和管线技能组合最朴素的模式是串行技能A的输出作为技能B的输入。实现上就是几行代码的事可靠可调试断在任何一环都知道去哪查。更复杂一点的是并行编排互不依赖的抽取任务同时跑最后做一个汇总merge。比如会议纪要技能和关键数字抽取技能可以并行执行最后合并成一份完整报告。并行模式能缩短延迟但对技能的无状态要求更高别在技能里偷偷改共享文件。当技能数量超过5个路由判断建议用显式的路由技能来做先让一个轻量LLM调用分析用户意图再决定走哪条技能串而不是把所有技能描述一股脑塞给主模型。这样做的代价是多一次模型调用换来的是路由准确率的大幅提升。5.3 技能复用与演进版本化、模式库、内部市场技能是可复用的资产就值得用资产的规格来管理。我在SKILL.md里加version字段每次改动行为都升版本号并在技能目录里放一份CHANGELOG记录变更。这个动作直接救过我一次有一次我改了分块参数周报技能跟着坏掉回头看CHANGELOG才定位到是参数调整连带影响了另一个技能的输入格式。团队里时间久了会沉淀出一批通用技能比如“翻译校对”“数据清洗”“URL内容抓取”。我会把这些技能集中放到一个技能仓库新项目直接复制目录使用而不是重新开发。有条件的话给技能写一行“适用场景”的索引README让团队搜索技能而不是重造技能。说点我自己的体会。踩过几次坑之后我越来越觉得Agent的技能工程本质上是在给模型做减负。你不用教会它所有步骤只需要让它知道有什么技能、什么时候用、怎么确认用对了。技能层不是万能的它解决的是稳定性和复用问题解决不了模型本身能力不足的问题。但如果你正在被“工具一多就乱、Prompt改一处崩全局”折磨我建议先从手写一个20行的小技能开始把“定义边界、写说明、写测试、挂载Agent”这条链路完整跑通再慢慢扩张技能库。这是我做了这么久Agent应用之后最想回头重做的一步。
返回列表