ARTICLE DETAIL

资讯详情

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

Agent技能集:让大模型自动化代理更稳定的工程实践

Agent技能集:让大模型自动化代理更稳定的工程实践 1. 设计思路为什么Agent需要一套“技能集”而不是一堆工具函数先说个背景。我最近半年一直在做基于大模型的自动化代理项目早期踩过一个特别典型的坑把十几个工具函数一股脑塞进系统的工具列表然后让Agent自己选。效果嘛单任务还行一旦场景复杂模型就开始“选择困难”明明该查数据库的它偏去调搜索接口该写文件的它跟你耗在参数补全上。而且每次新增一个工具旧任务的稳定性就可能被牵连。后来我把这套东西重构成了“Agent技能集”模式也就是这次想聊的agent-skills。所谓Agent技能集说白了就是把“工具函数 使用说明 触发场景”打包成一个独立单元。每个技能就是一个文件夹里面装着它专属的指令文档、脚本和依赖声明。模型不需要在每次请求里看到全部工具而是先根据用户意图命中一两个技能再把对应技能的说明加载进来使用。这个思路和传统function calling最大的区别在于它不是给模型发一把螺丝刀、一个扳手、一台电钻而是给模型一份“工种手册”需要拧螺丝时说清楚用哪个工具、按什么顺序操作、有什么注意事项。我之所以坚持这个设计有几个实际考量。第一个是上下文预算。大模型上下文窗口是很贵的资源。假设你有30个工具每个工具描述平均400个token光工具定义就吃掉12000个token这还没算系统提示词和对话历史。用户一次普通的查询上下文可能直接烧掉三分之一。而技能集模式下主提示词里只有一份十几个技能的索引列表每个技能一句话长描述总共不超过1500个token被命中后才加载单个技能的完整说明整体开销小得多。第二个是职责边界。传统工具列表里工具之间是平级的没有层次关系。Agent要自己判断“先做什么再做什么”。技能集天然带流程一个技能里可以包含多步操作顺序在SKILL.md中写明模型只要按步骤走就行。比如“网页摘要”技能内部要先抓取页面、清理正文、抽取标题、再调用摘要接口这些步骤封装在技能内部对外只暴露一层触发条件。第三个是复用和分享。工具函数写得再好换一个项目基本无法直接迁移因为提示词、参数定义、错误处理都是为旧项目定制的。技能集把指令和代码绑在一起拷贝整个目录到新项目就能用团队里共享也非常方便。这就很像npm包或pip依赖只是它捆绑的不只是代码还有模型侧的行为约定。这套设计的关键取舍在于技能粒度怎么定。太粗一个技能包含太多场景命中后指令模糊模型容易跑偏太细技能数量爆炸索引列表又变长命中准确率下降。我现在的经验是以“一次性任务”为粒度用户的一句话意图如果可以用不超过5步操作完成就值得做成一个单独技能。超过5步的任务建议拆成多个技能再串成工作流而不是硬塞进一个技能里。2. 目录结构、SKILL.md规范与触发机制的关键细节技能集的物理形态并不神秘就是一个约定好的目录结构。每个技能一个文件夹内部必须有一个SKILL.md这是模型的“说明书”其余是工具脚本、资源文件和配置。下面是我目前比较稳定的一套结构skills/ 01_web-summarizer/ SKILL.md tools/ fetch_page.py clean_html.py summarize.py assets/ prompt_templates/ short_summary.txt detailed_summary.txt requirements.txt 02_database-query/ SKILL.md tools/ run_sql.py config/ db_connections.yamlSKILL.md是整个技能集的灵魂。它决定了模型什么情况下会想到这个技能、加载后知道怎么正确执行。我推荐用YAML frontmatter Markdown正文的双段结构。frontmatter给调度器看正文给模型看。--- name: web-summarizer description: 当用户需要总结网页内容、提取文章要点、生成链接摘要时使用。适合处理博客、新闻、产品页面等公开网页。 version: 1.2.0 author: team-ai license: MIT triggers: - 总结网页 - 提取摘要 - 这篇文章讲了什么 readme: | 加载本技能后按 tools/ 目录下的步骤执行。先抓取网页正文再清洗为纯文本最后调用摘要模板生成输出。整个过程不要向用户询问多余参数URL 优先从对话上下文中提取。 --- # Web Summarizer 技能使用说明 ## 执行步骤 1. 从用户输入中提取目标URL。如果没有明确URL但有链接请解析为绝对地址。 2. 运行 python tools/fetch_page.py url获取干净的正文文本。 3. 判断正文长度。超过2000字时使用 detailed_summary.txt 模板少于2000字使用 short_summary.txt 模板。 4. 输出摘要时保留原文核心论点和关键数据不要添加外部信息。 ## 注意事项 - 只抓取目标页面本身不沿链接继续爬取。 - 如果页面返回404或超时直接告知用户抓取失败不尝试其他URL。 - 禁止将网页内容用于商业用途所有结果仅供个人学习参考。这里最关键的是description字段。你可以把它理解为技能的“触发开关”。模型不会逐字阅读所有技能正文它只根据用户请求和frontmatter里的description做快速匹配。所以description写得越精确技能被正确调用的概率越高。我的体会是写description要遵循三条原则。一是动词开头明确动作。尽量写“当用户需要总结某个网页时”而不是“包含网页摘要相关能力”。前者是任务导向后者是能力导向。模型做意图匹配时任务导向的描述命中率高得多。二是写清楚不做的事。description里加一句“不要用于PDF文件”“不要处理需要登录的页面”这类边界说明能把误调用率压下去。很多技能被乱用不是模型笨而是你没告诉它边界。三是避免过多同义词堆砌。我见过有人把“总结、摘要、概括、提炼、要点、核心内容”全部塞进description以为覆盖广结果模型反而困惑。一两个代表性动词加一个场景短语就够了。除了SKILL.md另一种我正在尝试的做法是给技能附带一个“技能索引文件”放在skills目录根部。这个文件不进入模型上下文而是给调度模块用一个轻量级模型或者基于embedding的检索器根据用户输入选出3-5个候选技能再把这几个候选的SKILL.md注入主模型。这样可以支撑上百个技能的大规模技能库避免所有技能的内容都塞给主模型。目前我在20多个技能的小型库上跑直接用单模型扫描frontmatter就够了技能超过50个建议上索引检索。3. 实操从零搭一个可用的“网页摘要”技能讲了这么多设计还是动手做一个完整技能最直观。以“网页摘要”为例我来完整走一遍流程包括目录创建、SKILL.md编写、脚本实现和联调验证。第一步确定技能边界。我给它定的范围是公开网页的正文抽取与摘要生成不处理需要登录的页面不访问无HTML正文的资源一次只处理一个URL。第二步创建目录和脚本。我的项目结构如下agent-skills/ app.py # Agent主程序负责调度 skills/ web-summarizer/ SKILL.md tools/ fetch_page.py summarize.pyfetch_page.py负责抓取网页并清洗为纯文本这是整个技能里最容易出问题的环节。我始终用双重清理策略先用正则粗粒度剥离script和style标签再用HTML解析库做精确恢复。实际代码如下#!/usr/bin/env python3 import sys import re import html import urllib.request from urllib.parse import urlparse UA Mozilla/5.0 (compatible; AgentSkillBot/1.0) def fetch_text(url: str, max_chars: int 8000) - str: parsed urlparse(url) if parsed.scheme not in (http, https): raise ValueError(仅支持 http/https 链接) req urllib.request.Request(url, headers{User-Agent: UA}) with urllib.request.urlopen(req, timeout10) as resp: raw resp.read().decode(utf-8, ignore) # 先去掉脚本和样式避免正文被噪声干扰 text re.sub(rscript[\s\S]*?/script, , raw, flagsre.I) text re.sub(rstyle[\s\S]*?/style, , text, flagsre.I) # 去掉标签和实体 text re.sub(r[^], , text) text html.unescape(text) # 压缩空白 text re.sub(r\s, , text).strip() return text[:max_chars] if __name__ __main__: try: print(fetch_text(sys.argv[1])) except Exception as e: print(fFETCH_ERROR: {e}, filesys.stderr) sys.exit(1)这个脚本的重点是异常处理。抓网页这种事目标服务器随时可能拒绝、超时、返回乱码。我把所有异常统一捕获并输出带前缀的错误信息这样Agent可以靠识别FETCH_ERROR前缀来给用户一个明确的失败反馈而不是把一串traceback丢给模型让模型猜发生了什么。summarize.py就简单一些接收抓取到的纯文本调用摘要模型接口按模板输出。我用的是一家通用模型服务的接口但为了演示这里用命令行参数传入文本的方式#!/usr/bin/env python3 import sys def summarize(text: str, max_len: int 300) - str: # 实际项目里这里会调用大模型接口 # 这里演示一个简单的提取式摘要逻辑 sentences text.replace(。, .\n).split(\n) kept [] total 0 for sent in sentences: if total len(sent) max_len: break if len(sent) 20: kept.append(sent.strip()) total len(sent) return .join(kept) if __name__ __main__: text sys.stdin.read().strip() print(summarize(text))第三步在SKILL.md中把这些脚本串成完整流程。之前已经给出了模板这里补充一点执行顺序和失败分支一定要写清楚。我见过很多技能文档步骤写得含糊模型自由发挥结果工具用错、参数传错。命令执行要转化为显式的Shell命令不要给模型选择空间。第四步写一个简单的调度器让Agent能命中这个技能。调度逻辑完全不复杂#!/usr/bin/env python3 import os import yaml SKILLS_DIR skills def load_skill_index(): index [] for name in os.listdir(SKILLS_DIR): md_path os.path.join(SKILLS_DIR, name, SKILL.md) if not os.path.exists(md_path): continue with open(md_path, encodingutf-8) as f: head f.read(2000) meta {} if head.startswith(---): _, fm, _ head.split(---, 2) meta yaml.safe_load(fm) or {} index.append({ name: meta.get(name, name), description: meta.get(description, ), path: md_path, }) return index if __name__ __main__: for skill in load_skill_index(): print(f{skill[name]}: {skill[description]})这个脚本的目的有两点一是供模型看到的技能索引列表二是方便人工检查每个技能的description是否写到位。我会在每次新增技能后跑一遍直接从输出结果判断描述质量不用打开每个文件。联调时我最常用的测试命令是这样的python app.py 帮我总结一下 https://example.com/chatgpt-usage 这篇博客Agent输出里应当出现技能名web-summarizer并且摘要内容取自目标页面正文。如果Agent没有命中技能而是直接瞎编我优先检查description是否覆盖了这个表达。这是我说的“先测触发再测执行”原则。很多团队在调试Agent技能时一上来就调试脚本本身结果代码没问题就是Agent不调用白忙一场。4. 常见问题与排查技巧实录技能不触发、误触发、上下文污染技能集做多了之后问题就不仅仅是写代码这么简单了。我整理了一套排查问题的方法集按出现频率排序给新手做个参考。现象可能原因排查方法修复方向Agent明明看到网页URL却不去调用web-summarizerdescription里没有覆盖用户表达方式查看技能索引输出确认描述与测试语句的语义匹配度重写description直接用测试语句里的原词Agent经常在用户没提网页时也调用该技能description边界描述缺失检查描述里有没有“不要用于XX场景”在description末尾加上明确排除项技能被命中但执行结果为空脚本异常被吞掉Agent拿不到错误信息单独运行脚本复现检查stderr输出确保所有异常都打印到stderr并带明确前缀页面内容很多上下文被撑爆脚本输出未做长度限制检查脚本返回的最大字符数设置max_chars优先截断正文而不是摘要两个技能都在说自己负责“网页处理”技能粒度设计重叠核对两个技能的description 触发词交集合并或重新切割技能边界先说技能不触发这个最常见的问题。我现在养成的习惯是写完一个技能先不看代码直接用十种不同的用户说法去测试触发。比如网页摘要技能我会试“总结一下这个页面”“这篇文章的核心观点”“把链接内容提炼一下”“这个博客讲了什么”等表达。如果一半以上没触发那说明description写得太窄。反过来如果用户在聊代码问题Agent却触发了网页技能说明description的边界没写清。排除问题时要区分两种上下文一种是“技能索引在系统提示里”另一种是“技能全文在上下文里”。前者是指模型能看到的技能目录后者是技能被命中后加载的完整SKILL.md。很多调试者搞混这两者以为把SKILL.md写到系统提示里就能解决问题结果上下文越来越长模型行为越来越飘。我的经验是索引和正文必须分开放索引保持短正文按需加载。再有一个极其隐蔽的坑脚本的输出污染了Agent的后续推理。早期的fetch_page.py会把抓取到的整页文本都print出来Agent收到后不仅包含了正文还混入了导航菜单、页脚、广告文案结果摘要质量一塌糊涂。后来我在脚本里只返回清洗后的正文并且加了字数上限问题立刻缓解。凡是工具脚本的输出都要比照着“这就是最终要交付给模型的信息”这个标准去做清洗。关于依赖环境我也踩过坑。一台服务器上Python版本不同、系统库缺失Skill里的脚本明明在本机跑得好好的部署后就报错。后来我把所有技能脚本统一用venv隔离requirements.txt声明依赖主程序通过固定的解释器路径调用。另外不要依赖当前工作目录脚本内部用绝对路径或者基于脚本所在目录的路径。技能集应该像一个可移植的盒子而不是只能在你机器上跑的临时脚本集合。说到误触发我有一个非常典型的例子。团队里同事给“会议纪要整理”技能写description写的是“当对话内容包含会议记录时使用”结果Agent在用户随口说“昨天开会说的事你记得吗”时也触发了这个技能把一篇正常聊天总结成了会议纪要。修复方法就是在description里加边界“仅当用户明确提供一段会议原始记录文本或录音转写文本时使用而不是根据对话历史推断会议内容。”这一条加完误触发率直接降了八成。我自己还维护了一个小型的触发回归集就是二十条典型用户句子以及期望命中的技能名。每次修改description跑一遍回归集对比命中结果。这个方法虽然土但在技能库规模不大时比任何评估框架都直观高效。5. 技能评估与迭代怎么让技能越用越准技能集不是写完就完了它更像一个需要持续打磨的产品。我目前的做法是每两周做一次技能体检重点看三个维度命中准确率、执行成功率、上下文消耗效率。命中准确率就是前面说的回归测试。我会根据线上日志抽取用户真实请求人工标注期望技能然后跑一遍当前技能索引统计正确命中的比例。这个数值低于80%说明技能描述体系需要调整不是改一两个description就行的可能要重新审视技能边界划分。执行成功率看的是技能内部脚本的健壮性。我准备了一个自动化冒烟测试每个技能至少有一个固定的测试用例比如web-summarizer就固定抓取一个我控制的测试页面。每次改完代码跑一遍所有技能的冒烟测试确保没有因为改动影响了已有技能。上下文消耗效率是我近来比较看重的一个指标。同样一个任务技能索引设计得好两轮对话就完成了设计不好模型需要向用户追问参数、反复试错四五轮还没结束。我在日志里记录每轮对话的工具调用次数和上下文token消耗异常波动说明技能的说明文档写得不够清楚模型拿不到足够信息去做一次成功调用。配合评估我还做了一套版本控制每个技能发布时SKILL.md里记录版本号和变更说明。模型执行时如果发现行为不符合当前版本预期我能在日志里快速定位是哪个版本的改动引起的。这一点在多人协作时特别重要——大家都改同一个技能没有版本控制出现问题根本扯不清。再看看技能集后续可以怎么扩展。目前我的项目已经支持了技能依赖就是一个技能可以声明依赖另一个技能调度器会自动加载被依赖的技能说明。比如我搭了一个“资讯简报”技能它内部依赖“网页摘要”“RSS抓取”“邮件发送”三个子技能用户只需要说“生成一份今日AI资讯简报”系统自动串联执行。这种多技能组合是我的主攻方向因为单个技能能力有限技能之间的编排才能释放Agent真正的潜力。对于一个刚开始用技能集的人我给一个最直接的建议先做一个小而精的技能库只放五六个真正高频使用的技能把pet每个SKILL.md都写到“连一个实习生都能照着执行”的程度再考虑扩大规模。技能集最大的陷阱不是做不出来而是做完以后没人用、没测准、没法迭代。它本质上是一个需要持续运营的体系而不是一次性开发的一组脚本。我个人现在的习惯是任何新的Agent项目动手写第一行业务代码之前先把skills目录建好把一个最小技能的SKILL.md写出来。有了这个骨架后续增加能力、排查问题、评估效果都有了一个清晰的落点。这是我在多次踩坑之后沉淀下来的最核心的工作方式。
返回列表