ARTICLE DETAIL

资讯详情

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

从提示词到技能库:AI Agent高效开发实战指南

从提示词到技能库:AI Agent高效开发实战指南 做AI Agent应用开发这段时间我最大的感受是Agent的能力边界很大程度上不是由模型决定的而是由你给它准备了多少技能决定的。这里说的agent-skills不是让模型多背几段提示词而是一整套把能力封装成可复用、可管理、可调用的技能体系。最近我把一个项目里的Agent从单一Prompt硬扛重构成技能库驱动效果可以说是天壤之别——同样一个任务改造前模型经常答非所问改造后基本一次命中。这篇文章我想把从零搭一套Agent技能库的完整过程分享出来包括为什么必须走技能化这条路、技能协议怎么设计、一个高质量技能到底该怎么写、调用链路上有哪些工程细节、以及技能库上线后怎么持续迭代。适合正在做Agent应用、被提示词膨胀和调用不稳定折磨的工程同学参考。1. 为什么Agent需要技能而不是提示词先说一个我踩过的坑。早先我做了一个信息整理类Agent把所有功能都堆在系统提示词里你是一个信息整理助手请使用以下步骤处理内容……然后后面跟着三四百行的指令。一开始在小规模测试里表现还行一旦任务类型变多问题就全冒出来了。1.1 提示词方案的隐性成本第一个问题是上下文被严重浪费。模型每次请求都要带着那几百行规则加上用户输入、历史对话、检索到的参考材料几百行的规则占了大量上下文窗口。更麻烦的是不同任务之间的规则会产生干扰——处理周报时模型可能莫名其妙执行了翻译成英文那一节里的指令因为这套规则在它看来都是同一段文本里的线索。第二个问题是没有办法针对性测试。提示词是一个整体你改了其中一小段关于输出格式的描述很可能影响它在另一个任务上的表现。回归测试完全没法做因为没有一个明确的功能边界。第三个问题是维护成本。提示词一旦超过两三百行再想改就得小心翼翼你不敢删任何一段因为你不知道删了之后哪块功能会崩。我见过团队的Prompt文件从几百行膨胀到上千行最后没人敢动它。1.2 技能的本质把会做变成能做人类员工入职之后公司不会把工作手册全部背下来而是给你一个岗位职责、几个常用工具、一份SOP需要什么查什么。技能化的思路是一样的Agent不需要知道所有事情怎么做只需要知道自己有什么技能可以用具体干活的时候由技能模块去执行。一个技能本质上是一个独立封装的能力单元包含三样东西技能描述description告诉模型这个技能是干什么的、什么时候该用它。参数定义parameters规定调用这个技能需要提供哪些输入格式是什么。执行体handler真正干活的部分可以是代码、命令行工具、外部API调用甚至是一个子Prompt。模型要做的事情被简化了它不再需要记住处理内容时第一到第七步怎么做只需要识别场景、选择合适的技能、填对参数。1.3 什么时候该做技能不是所有东西都要技能化。我自己的判断标准是三条这个能力是否会被重复使用。只跑一次的临时逻辑直接写在代码里就可以。这个能力是否有明确的输入输出边界。比如将Markdown转为PDF提取网页正文计算两个日期之间的工作日都很清晰。模糊的能力比如理解用户情绪不适合先做成技能。这个能力是否需要独立测试和迭代。凡是需要单独调优、验证、升级的都应该从Prompt里拆出来。这三条在功能够不够用这件事上是分水岭。把能拆的都拆成技能之后模型的主Prompt可以变得非常短系统提示词只需要指定身份、语气、总体的工作流程剩下的全部交给技能路由。2. 技能库的顶层设计先定协议再写实现确定了技能化这个方向之后第一件事不是急着写技能而是先把技能库的协议定下来。协议决定了你的技能库能长多大、能不被自己乱死。2.1 技能的通用结构我的做法是每个技能用一个独立目录承载目录里包含一份SKILL.md清单文件和若干实现文件。SKILL.md是模型唯一会读到的文件它决定了模型能不能正确理解并使用这个技能。一个典型的结构长这样skills/ content/ extract_web_page/ SKILL.md handler.py requirements.txt examples/ sample_output.md data/ csv_to_json/ SKILL.md handler.py misc/ calculate_workdays/ SKILL.md handler.pySKILL.md内部我固定用以下几个区块--- name: extract_web_page description: 从网页URL抓取内容并转换为结构化Markdown保留标题、正文、表格等核心信息 --- ## 使用场景 - 用户给了一个URL需要阅读网页内容时 - 需要从网页中提取正文、列表、表格数据时 ## 参数说明 - url: 网页完整URL必须包含协议头 - include_tables: 是否保留表格结构默认 true ## 输出格式 返回Markdown文本包含来源URL、标题、正文、表格如有 ## 注意事项 - 仅支持可公开访问的页面 - 验证码页面会返回错误提示 - 抓取失败时返回错误码 ERR_FETCH_FAILED并附带原因这里的关键是description字段。模型看到技能列表时的决策完全依赖这段文字写得好不好直接决定模型会不会选错。2.2 用分级目录管理技能技能数量一旦超过二十个就得有分组概念。我把技能按能力领域分目录content内容处理、data数据转换、web网络交互、misc杂项。分组的作用不只是文件整理更重要的是在构建技能列表时可以用目录名作为技能的前缀让模型通过前缀快速理解技能归属。比如content.extract_web_page和content.summarize_article放在一起模型看到content前缀就能猜测这是内容处理相关的能力如果以后加一个data.json_to_csv明显不是同一类模型也就不容易混淆。2.3 技能间的依赖关系处理技能之间一开始是独立的但随着场景变复杂会出现依赖需求。我在项目里遇到过extract_web_page抓下来的内容下一步经常要喂给summarize_article做总结。有两种处理方式一种是把依赖写死比如summarize_article内部直接调用extract_web_page。这种方案简单但把两个技能耦合在一起后续很难单独复用。另一种是设计组合流程由Agent在工作流层面先调用extract_web_page拿到结果再把结果作为summarize_article的输入。这种方式更灵活缺点是模型需要自己判断流程编排偶尔会漏步骤。我的经验是底层原子技能保持纯净组合编排交给一个调度型技能去做。调度型技能本身也是技能它的执行体是一个明确的任务流描述模型负责按流程走。这样既保留原子技能的复用性又让复杂场景可控。3. 手写一个高质量技能以网页内容结构化提取为例理论说多了容易飘我用项目里一个实际技能来演示完整的实现过程。这个技能解决的需求很常见用户丢一个URL过来Agent需要读取网页内容然后才能继续做总结、翻译、信息提取等后续动作。3.1 需求拆解大多数网页并不仅仅包含正文还有导航栏、侧边栏、广告位、页脚等噪音信息。直接抓取HTML再喂给模型上下文浪费严重而且模型容易被干扰。所以技能的核心是去噪结构化。我把需求拆成三个子需求根据URL抓取原始HTML解决HTTP请求、编码识别、超时处理。从HTML中提取正文主体内容去掉导航、脚本、样式和页脚的噪音。将提取结果转换为干净的Markdown格式保留标题层级、段落、表格和图片链接。3.2 技能描述怎么写模型才容易命中这是最容易被忽略的环节。我见过很多人写description时只写一句抓取网页并转换格式结果模型在该不该用这个技能上经常犹豫不决。我在extract_web_page的description里明确写了两部分功能描述 使用条件判断。功能描述要动词开头、说明输入输出使用条件判断要告诉模型哪些情况下绝对不要用这个技能。description: 从网页URL抓取内容并转换为结构化Markdown。当用户提供URL链接、需要读取网页内容时应使用此技能。仅当用户明确要求使用指定API或指定抓取工具时才考虑其他方式。仅当……才考虑其他方式这种约束非常有用它能帮模型在多个相似技能之间做排除。3.3 参数定义与执行体实现参数定义我用了JSON Schema{ name: extract_web_page, parameters: { type: object, properties: { url: { type: string, description: 网页完整URL必须包含http或https协议头 }, include_tables: { type: boolean, description: 是否保留表格结构默认true。当网页内容包含数据表格且用户需要结构化数据时保留 } }, required: [url] } }参数少而精只有一个必填项、一个可选项。参数太多会让模型填起来费劲也容易填错。执行体我用了Python主要是两个库requests做抓取readability-lxml做主内容提取。import requests import readability from bs4 import BeautifulSoup def extract_web_page(url: str, include_tables: bool True) - str: headers {User-Agent: Mozilla/5.0 (compatible; AgentBot/1.0)} resp requests.get(url, headersheaders, timeout15) resp.raise_for_status() doc readability.Document(resp.text) summary_html doc.summary() soup BeautifulSoup(summary_html, html.parser) # 表格处理如果不需要表格直接移除 if not include_tables: for table in soup.find_all(table): table.decompose() # 转换为Markdown markdown html_to_markdown(str(soup)) return f来源: {url}\n\n{markdown}一个容易踩的坑是响应编码。很多网页的编码不在HTTP头里标识requests的resp.text会默认用ISO-8859-1解码结果中文全乱码。我的做法是优先从HTML的meta标签里取charset取不到就用chardet自动检测。3.4 返回结果的标准化技能的输出一定要统一格式否则下游处理没法做。我固定了三种返回形态情况返回内容成功以来源: URL开头的Markdown正文页面不存在/请求失败ERR_FETCH_FAILED: 状态码 404不是文章页面/正文为空ERR_NO_CONTENT: 页面未能提取到有效正文标准化输出最大的好处是Agent拿到结果第一眼就能判断下一步怎么做看到ERR前缀直接回复用户失败原因不用硬分析残缺内容。4. 技能调用链路上的工程细节技能本身写好了只是第一步。真正让技能库好用的是调用链路的工程化水平。我在这个环节踩过不少坑挑几个典型的说。4.1 路由选择模型怎么知道该用哪个技能模型拿到的技能列表本质上是一堆名字描述。如果技能数量超过十个模型在选择时会出现两类问题同类技能认不出差异、完全不相关的技能干扰判断。解决办法是一个叫技能路由提示的小技巧。我会在系统提示词里加一段可用技能按需调用。选择时优先匹配使用场景描述如果存在两个相似技能对比它们的与不同描述任何技能都不匹配时直接告知用户无法处理不要强行选择。同时给每个技能加一个与XX技能的不同字段如果它和其他技能有重叠。这个字段写起来很费神但能显著降低模型选错概率。4.2 上下文管理与状态传递技能调用过程中中间结果怎么传递至关重要。我见过最糟糕的写法是技能结果直接塞回对话历史下一次调用再把整个对话历史发给模型上下文很快撑爆。我的做法是维护一个独立的技能执行缓冲区技能的输入、输出、执行状态都放在这个缓冲区里不进主对话历史。Agent做完一步我只把该步骤的结论摘要写回主对话历史。这样既保留工作流的可追溯性又不会让上下文无限膨胀。例如抓取网页之后写回主对话的是一句话已获取该网页内容正文共3420字包含1个表格关键主题为……而不是整段网页内容。4.3 并发、超时与失败回退技能执行体是耗时的外部调用场景必须设计超时和失败回退。我把技能执行分为同步和异步两种模式同步技能预期在3秒内返回结果的短操作。超时时间设为10秒超过直接报错。异步技能耗时较长、需要轮询结果的操作比如批量转换、长文档处理。立即返回一个任务ID后续通过查询接口获取结果。失败回退也很重要。比如网页抓取失败除了报错之外我会尝试一次无脚本请求的回退很多站点对带完整JS环境的请求有防御但普通requests请求反而能拿到内容再不行才报错。这个回退逻辑直接写在handler里不需要模型介入。4.4 技能的可观测性技能库上线前期最痛苦的事情是你根本不知道模型是不是正确调用了技能。所以我在每次技能调用时都会打点记录调用了哪个技能模型填了哪些参数执行是否成功、耗时多少返回结果长度、是否带ERR前缀这些日志汇总之后我能直接看到每个技能的实际使用频率、失败率、平均参数量。一个技能如果被调用了100次但只有40次成功那就说明执行体有问题或者参数定义让模型困惑。这个数据驱动的迭代方式比靠感觉改Prompt靠谱得多。5. 技能库失效的典型场景与迭代方法技能库上线不等于一劳永逸。运行一段时间之后你会发现有些技能开始失灵表现是模型该用的时候不用、不该用的时候乱用或者技能执行体本身在新场景下报错。总结下来我遇到的失效场景主要集中在三类。5.1 技能不生效的三种常见原因第一类description描述与用户实际表达之间的gap。模型是根据用户的自然语言去联想技能的。如果用户说帮我把这个网页存下来而技能的description只写了抓取网页并转换为Markdown模型可能不认为存下来和抓取是同一个需求。后来的修正方法是把常见用户说法直接写进description的使用场景里例如用户说存一下这个页面 这个链接打开看看 把网页内容整理出来时使用。第二类参数定义太严格或太宽松。有一个技能的参数里我对日期格式做了严格的YYYY-MM-DD要求结果模型在用户说这周五时不知所措因为它是LLM不具备日期计算能力填不出这个参数。后来我把所有需要日期计算、单位换算的参数定义修改为两个版本如果模型能直接提取就填写具体值否则填入一个待转换标记由执行体内部做计算。第三类技能执行体本身的脆弱性。例如网页抓取技能遇到需要登录的页面、遇到前端渲染的SPA页面时都会失败。这类问题不是改描述能解决的需要在执行体上做增强或者明确在description里声明不支持需要登录的页面让模型提前知道边界避免反复失败。5.2 用评测集守住技能回归技能库规模一大最怕改A技能、坏B技能。我建立了最基础但非常管用的回归评测机制给每个技能准备一套golden任务集。golden任务集不需要很大每个技能5个典型任务即可但覆盖面要够。例如extract_web_page我会准备5个URL分别包含正常图文页、带大量表格的页面、带有导航噪音的页面、不存在的URL、返回非HTML文件的URL。每次技能改动后跑一遍golden集确认所有任务的结果没变差。这一个机制救了我好几次。有一回我只是调整了技能路由提示的词序结果某个场景下模型开始跳过extract_web_page直接乱答。回归测试第一轮就抓到了这个问题回滚得比较及时。5.3 从单个技能到技能家族的演进技能库成熟的标志是不再是零散技能的集合而是形成了技能家族。比如extract_web_page之后我逐渐发展出extract_web_page → extract_table → extract_article_metadata这一组内容提取系列技能。它们共享底层的抓取模块只是输出的结构化程度不同。我的建议是当技能数量超过30个、或者同一领域出现3个以上技能时就主动做一次技能合并和拆分演练。合并是把重叠度过高的技能收编成一个带模式参数的大技能拆分是把一个描述冗长、使用条件复杂的技能拆成多个单一职责的技能。权衡标准始终是模型是否容易决策。技能太多模型决策负担大技能太少单个技能边界模糊也容易选错。我现在的体量是40个左右技能分组为7个领域目录模型侧的效果比较稳定。我在实际项目中体会最深的一点是Agent技能库的搭建不是一个一次性的开发任务而是一个持续的数据驱动过程。你把技能协议定好、把观察点埋好、把回归集建好剩下的就是不断地根据真实调用日志去修正技能描述、调整参数定义、增强执行体的边界处理。做到这个程度之后一个Agent项目的新需求开发节奏会变得很快——大部分情况下只是往技能库里多放一个目录、写一个SKILL.md、跑一遍回归就完事了。这种标准化带来的确定性是纯Prompt方案给不了的。
返回列表