拓冰建站拓冰建站
首页 / 资讯中心 / 正文

把技能当作第一公民:Agent结构化技能管理实战解析

2. 项目定位为什么我把“技能”当成Agent的第一公民先说个我自己的观察。我做过好几个基于大模型的Agent项目早期最头疼的问题不是模型不够聪明而是“模型拿着40个工具却经常选错、乱调、甚至把参数塞得乱七八糟”。后来我把注意力从“堆工具”转到“管技能”事情才有了质的变化。“agent-skills”这个项目就是干这件事的它是一套结构化的Agent技能管理方案核心思想是把每个能力比如查天气、读PDF、写SQL、发邮件从零散的函数调用封装成带有名称、描述、输入输出Schema、调用约束、示例的“技能包”再通过一个轻量级调度层让大模型按需加载、按约束执行。这套方案解决的核心问题有三个选择困难工具一多模型分不清“查天气”和“查温度趋势”有什么区别技能描述写清楚后选择准确率明显提升。上下文爆炸把几十个工具的完整说明全塞进系统提示词几千个token就没了。技能库只把“当前任务相关的3~5个技能”注入省下的空间全留给上下文。复用性差以前每个项目里都有一份“处理时间格式”的函数换个项目又要重写。技能包可以独立维护、跨项目复用这才是“资产”该有的样子。适合看这篇内容的人包括正在用LangChain、AutoGen、Claude或自研框架搭Agent的开发者被“工具调用不稳定”折磨的AI应用工程师以及想给自己的Agent做一套可扩展能力体系的架构师。如果你想做的只是“调一次API的demo”那这篇文章对你可能偏重但如果你想让Agent真正稳定地干活这套思路值得看完。3. 整体架构分层、解耦、可插拔3.1 技能库的三层结构我把整个agent-skills拆成三层每层只干一件事层与层之间用标准接口连接这样替换任何一层都不会影响其他层。第一层是技能定义层。这一层是一堆目录和文件每个技能对应一个文件夹里面包含SKILL.md技能说明书、schema.json参数规范、run.py或run.js执行脚本、examples/示例集。这一层是给人看的也是给模型“读说明书”用的。它的关键点是“自包含”——一个技能文件夹从另一台机器clone下来就能跑不依赖全局状态。第二层是技能装载层。这层是一个服务负责扫描技能目录、解析元数据、建立索引并根据当前任务把相关技能注入到模型的上下文里。你可以理解成“技能版的搜索引擎”输入是用户请求输出是3~5个最匹配的技能包。它还要做版本管理比如某个技能升级了参数格式旧调用方不必立刻改代码。第三层是执行与编排层。模型决定调用哪个技能后这一层负责校验参数、执行脚本、处理超时和错误并把结果格式化后回传给模型。如果任务需要多个技能协作比如“读取邮件里的PDF提取表格然后生成周报”还由这一层做顺序编排。我之所以坚持分层是因为在实际项目里吃过“一锅端”的亏最早把所有工具逻辑写在一个Agent类里结果每次加一个新能力都要动核心代码回归测试跑一轮心累。分层之后“加技能”变成“丢一个文件夹进去”核心调度逻辑几个月都不用改一次。3.2 为什么不用“全量注入”而用“动态装载”有人会问直接把所有技能描述拼进system prompt不就行了何必搞一个装载层我在一个中等复杂度的项目里实测过40个技能每个技能描述平均200字加上参数Schema和示例光工具说明就接近1.2万token。模型每次请求都要“读”一遍这些内容响应时间增加30%以上而且因为信息过载工具选择准确率反而会掉5~8个百分点。动态装载就是针对这个问题做的优化。它分两步先用轻量级规则或embedding把100个技能缩小到5个候选再把5个候选的完整说明注入上下文。这一步在工程上像“检索增强生成RAG”但检索的不是知识文档而是“能力说明书”。具体做法每个技能在元数据里维护一组关键词和场景标签比如[pdf, 表格提取, OCR]任务进来后先做一次关键词匹配再取embedding相似度Top K两张榜单做加权融合。这个策略我用了很久实测能把工具选择准确率从87%提到96%左右代价只是增加了一次向量检索耗时约20毫秒完全可以忽略。3.3 目录结构的落地参考下面是我在实际项目里用的目录结构你可以直接抄agent-skills/ ├── skills/ │ ├── pdf_table_extractor/ │ │ ├── SKILL.md │ │ ├── schema.json │ │ ├── run.py │ │ └── examples/ │ │ ├── input_sample.pdf │ │ └── expected_output.json │ ├── sql_generator/ │ │ ├── SKILL.md │ │ ├── schema.json │ │ ├── run.py │ │ └── examples/ │ └── email_sender/ │ ├── SKILL.md │ ├── schema.json │ ├── run.py │ └── examples/ ├── loader/ │ ├── indexer.py │ ├── retriever.py │ └── schemas.py ├── executor/ │ ├── runner.py │ ├── validator.py │ └── error_handler.py └── tests/skills/目录每一级都比较浅“一个技能就是一个小项目”的体感非常舒服。loader/和executor/是通用代码不感知具体技能这也是“可插拔”的保证。4. 技能元数据设计让模型“看懂”技能的关键4.1 元数据字段与作用技能说明书SKILL.md是整个技能库的灵魂我建议至少包含以下几个字段缺一个都可能在实际调用时出问题字段作用我的建议name技能唯一标识用蛇形命名如pdf_table_extractor不要用带空格的短语description技能功能的自然语言描述写“能做什么、不能做什么、典型场景”控制在150字以内input_schema参数的JSON Schema严格定义类型、必填项、取值范围模型靠它做参数补全output_format输出结构定义明确返回的是JSON、文本还是文件路径error_codes常见错误码说明让模型在技能失败后能自己决定“重试”还是“换方案”permissions需要的权限声明如network: yes、file_read: whitelist、email_send: confirmexamples1~3个典型调用示例这是模型学习的“少样本样例”强烈建议写description和examples的重要性经常被低估。我见过不少项目description只写一句“处理PDF”结果模型在需要“PDF转图片”时也去调它因为转图片的诉求被模型“脑补”成了PDF处理。把边界写清楚反而能减少错误调用。4.2 Schema定义的两个原则第一原则是参数宁少勿多。模型在生成JSON时参数越多越容易出错尤其是嵌套对象很容易少个括号或多一个没定义的字段。我通常把一个技能的参数控制在5个以内超过5个就考虑拆成两个技能。实在拆不掉的用properties里的default值兜底而不是全部做成必填。第二原则是枚举值写清楚。比如“输出格式”参数如果允许的值是[json, markdown, csv]那就必须在Schema里写死枚举而不是在描述里写“根据用户需要输出”。模型对自由发挥的字段往往把握不准枚举能帮它锁定选择。下面是一个简化版的schema.json示例字段不长但关键信息都在{ name: pdf_table_extractor, description: 从PDF文件中提取表格数据支持扫描件OCR返回结构化JSON。仅适用于表格型PDF不适合提取正文段落。, input_schema: { type: object, properties: { file_path: { type: string, description: PDF文件的本地绝对路径 }, page_range: { type: string, description: 页码范围如1-3或2默认全部 }, ocr: { type: boolean, description: 是否启用OCR识别扫描件, default: false } }, required: [file_path] }, output_format: { type: array, items: { type: object, properties: { page: { type: integer }, table_index: { type: integer }, headers: { type: array }, rows: { type: array } } } }, examples: [ { request: 提取 report.pdf 第2页的表格并识别扫描内容, response: { file_path: /data/report.pdf, page_range: 2, ocr: true } } ] }这里有个细节examples里的request是“用户原话的样子”response是“技能应该接收到的参数”。模型的少样本学习恰恰就是靠这种配对学到的——它会把“用户口语”映射成“结构化参数”。如果少了这一步模型经常把用户原话直接塞进file_path。4.3 技能描述的“边界写作法”写description这件事我踩过不少坑总结出一个“边界写作法”先写能做什么再写不能做什么最后写典型场景。举个对比差处理PDF文件好从PDF中提取表格支持OCR。不适用于纯文本提取、不适用于图片转PDF。典型场景财务报表、学术论文数据表、扫描件表格提取。前一种描述下模型遇到“帮我把这个PDF的文字提取出来”也可能调它然后返回一个空结果后一种描述里模型知道“提取正文文字”应该找别的技能即使库里没有它也会明确告诉用户“没有这个能力”而不是硬调一个做错的。这个写法损耗很小收益却很大。我在一个客服场景的Agent里应用后工具误调用率降低了约60%。5. 核心实操从零搭建一个可用的技能库5.1 环境准备与基础框架选择我实际用的技术栈是Python 3.11 FastAPI LangChain仅用它的工具调用协议但这里要强调agent-skills的核心是协议设计不是某个框架。你用Node.js、Go也能做只要把SKILL.md和schema.json的解析、注入、执行逻辑理顺就行。我用FastAPI主要是图它的类型校验和OpenAPI文档生成省不少事。依赖安装用pip就行pip install fastapi uvicorn pydantic pyyaml openai如果技能里涉及PDF处理再按需加pypdf、pdfplumber、pytesseract这类库。不建议在一开始就把所有可能的依赖都装上技能库的依赖应该跟技能走而不是跟主程序走。5.2 技能装载器的实现思路装载器就是那一层“把技能从目录变成索引再按需检索”的服务。我用两个文件实现indexer.py负责启动时扫描目录、解析元数据、建立内存索引retriever.py负责运行时根据任务文本召回Top K技能。indexer.py的核心逻辑分三步遍历skills/目录下的所有子目录逐个解析SKILL.md里的YAML头我习惯在SKILL.md顶部放一小段YAML元数据正文放给模型看的长描述然后用Pydantic模型校验字段。校验不通过的技能直接跳过并输出warning——这一步很关键避免某个技能格式写错导致整个服务启动失败。retriever.py是我最常调优的部分。一开始我用纯关键词匹配效果比较差因为用户表达可能跟技能描述用词不一致。后来改成“关键词匹配 embedding相似度”的双通道才稳定下来。embedding模型我用的是text-embedding-3-small单次调用成本忽略不计。两个通道的打分规则很简单命中一个关键词标签加1分语义相似度的具体数值我会按0.6的权重转换总分从高到低排序取前5个技能。这里有个容易忽略的点检索结果的排序不需要特别精确只要把“真正可能需要的技能”召回进来就行。因为最终决定调用哪个技能的是大模型装载器只负责缩小选择范围。所以检索策略宁可召回多一点也不要漏掉可能相关的技能。实测中Top 5的召回率能到96%左右已经够用。5.3 执行器与参数校验执行器是技能实际跑起来的地方也是最容易出问题的地方。我踩过最典型的坑是模型生成了参数JSON但类型不对——比如page_range本来是1-3的字符串模型传成了1。Pydantic的校验在这里帮了大忙它能在执行前拦截掉明显错误的数据返回给模型一个“参数校验失败请修正”的提示而不是让脚本带着错误参数跑一半崩掉。执行器的流程我设计成四步解析参数 → 校验Schema → 执行run脚本 → 格式化输出。执行run脚本时用subprocess隔离设置超时时间默认120秒防止某个技能死循环拖垮整个服务。错误处理也很重要技能的错误码要能回传给模型否则模型不知道为什么失败也就无法自行修复。这四步看起来简单但在真实场景里能挡住很多问题。我把参数校验放在执行之前而不是执行之中就是因为“让模型重新生成参数”比“让技能脚本自己处理脏数据”要可靠得多。5.4 最小可用的调用流程写一个最小Demo让你直观感受整个流程。假设用户说“把report.pdf第2页的表格提取出来那个页面是扫描件”。from app.loader.retriever import retrieve_skills from app.executor.runner import execute_skill user_task 把report.pdf第2页的表格提取出来那个页面是扫描件 correlated retrieve_skills(user_task, top_k5) for skill in correlated: if skill.name pdf_table_extractor: result execute_skill( skill, { file_path: /data/report.pdf, page_range: 2, ocr: True, } ) print(result) break实际项目中这里不是“遍历技能”而是“把候选技能交给大模型做函数调用决策”。但局部看流程就是这三步检索候选、锁定目标、执行并返回。整体上装载器、执行器、模型决策各司其职调试时也能快速定位问题出在“没召回”还是“调错参数”还是“脚本报错”。6. 技能编排让多个技能协作完成复杂任务6.1 顺序编排与条件编排单个技能只能解决单一问题真实业务往往是“组合拳”。比如“读取邮件附件里的PDF表格汇总数据生成周报并发邮件”这里至少涉及pdf_table_extractor、sql_generator、email_sender三个技能。我做编排的原则是能靠模型做决策的就交给模型节奏控制、失败回退这类确定性的逻辑交给代码。也就是说编排层不为“下一步该调哪个技能”做硬编码而是把前一步的输出整理成上下文让模型决定下一步调用谁。但“最多执行几步”“超时怎么办”“某个技能失败后是否继续”这类边界条件由编排层强制约束。顺序编排最简单的实现是“循环决策”模型每一步选择一个技能执行器执行把结果追加到对话上下文再让模型决定下一步。条件编排稍微复杂一点比如“如果PDF提取的结果为空则不执行SQL生成而是直接回复用户”这种分支逻辑我建议显式写出来不要指望模型自己判断。6.2 技能间数据传递的格式约定技能之间协作最难的是数据格式统一。早期我遇到过pdf_table_extractor输出的表格是列表嵌套字典sql_generator却接收CSV字符串导致中间的转换代码比技能本身还复杂。后来我强制规定技能之间的数据传递统一用JSON且每个技能在output_format里声明自己的输出结构。任何技能需要消费上游数据时第一件事是写一个“适配器”把上游输出转成自己能接受的格式。虽然多写几行代码但每个技能保持了解耦不会因为另一个技能改了输出格式而跟着改。举个实际的链接方式pdf_table_extractor - { tables: [{ headers: [...], rows: [...] }] } sql_generator - { sql: CREATE TABLE ..., execution_plan: ... } email_sender - { status: sent, message_id: ... }这样链式调用时每个技能消费上一环的JSON产出下一环需要的JSON数据流一目了然。排查问题时也能直接看哪一环产出的JSON不符合预期不用翻遍代码。6.3 编排中的约束与权限控制技能编排时还要考虑一个容易被忽略的点技能不是无条件可调用的。比如email_sender这种涉及对外发送的敏感操作不能模型一选就执行得有人工确认或权限校验。我在每个技能里加了permissions字段编排层在执行前检查当前会话的权限范围。比如普通用户的会话允许调用pdf_table_extractor和sql_generator但调用email_sender时必须弹人工确认只有管理员会话能直接发送。这个设计在内部工具场景尤其重要能避免Agent“好心办坏事”。更细一点的约束是“资源配额”。比如某个技能单次执行耗时很长或者依赖外部API配额控制能防止Agent因为循环调用把账单打爆。我在编排层加了一个简单的计数器单次任务中每个技能最多调用3次超过后必须向用户说明原因。这个逻辑写起来就十行代码但真的能省不少钱。7. 踩坑实录与性能调优实战7.1 典型报错、定位思路与解决方案这节是真实项目里踩出来的“血泪史”每条都值得反复看。我把高频问题和排查思路整理成了速查表现象根因排查方法解决方案模型选错技能技能描述边界不清、命名太像看模型调用日志确认候选技能列表用“边界写作法”重写description改名拉开差异参数偶尔多一个字段Schema没有枚举限制查看模型生成的原始JSON用Pydantic严格校验拒绝未知字段技能执行成功但结果不对上游技能输出格式没对齐打印每步输出JSON引入适配器强制JSON格式传递检索召回不到相关技能标签太宽泛或embedding维度不对测试几条不同表达的任务扩充标签换用更合适的embedding模型系统提示词超长技能描述太长或注入个数太多检查token用量的统计日志压缩描述Top K从5降到3固定时间点超时某技能依赖外部API变慢分技能记录执行耗时单独调大超时时间或加缓存其中“模型选错技能”是出现频率最高的问题而且改Schema无法彻底解决——因为模型是根据语义而不是字段来理解的。唯一有效的办法就是反复打磨每个技能的description和examples。这块没有银弹只能靠测试集积累。第二个高频问题是“检索召回不到”。我早期用纯关键词匹配时用户说“把扫描件里的表格转出来”技能描述里只有“OCR”就是匹配不上。后来把tags扩充成一组同义词[OCR, 扫描件, 图片文字, 文本识别]召回率立刻好看了。扩展同义词这事可以一边用一边积累不用一次性做全。7.2 技能加载与执行性能优化性能调优要区分“加载时”和“运行时”。加载时最耗时的是初始化embedding模型和扫描磁盘上的技能文件。embedding模型我建议做成懒加载——服务启动时不加载第一次需要检索时才加载这样冷启动时间从8秒降到1秒以内。扫描技能目录也可以用增量索引只记录文件hash启动时对比hash没变动的技能跳过解析。运行时主要优化点是“减少不必要的重计算”。比如同一个技能在同一个会话里被反复调用时上次执行的结果可以缓存embedding检索结果也可以做短期缓存用户连续追问相似问题时能直接命中。我实测加了一个简单的LRU缓存后检索呼应的平均延迟从50ms降到了10ms左右。还有个容易被忽视的优化技能执行结果里的大字段比如提取出的整张表格没必要全量回传到模型上下文。可以精简成“前N行 统计信息”让模型知道结果概要就够了需要明细时再按需读取。这样能大幅降低token消耗响应速度也明显提升。7.3 上下文窗口管理的实用经验上下文窗口满了是所有Agent应用都会遇到的问题。技能库方案能缓解但不能根治核心思路还是“只放必要信息”。我的做法是三级上下文策略第一级是会话级放系统角色说明和用户核心意图第二级是技能级放当前候选技能的描述和Schema第三级是结果级放最近一次技能执行的精简结果。每级都有自己的“过期策略”——会话级的能留多久留多久技能级的每轮任务结束后清空结果级只保留最近两轮。这个策略在长会话场景里效果很明显。我在一个“数据分析助手”项目里以前跑到第20轮对话时系统提示词加历史消息已经占了2.6万token模型经常开始胡言乱语。改造后每轮固定只保留“1条用户意图 3个技能说明 2条执行结果”token占用稳定在6000左右模型到第40轮依然稳定。8. 测试与发布怎么保证技能库越改越稳8.1 技能级测试与回归测试设计技能库跟普通代码库一样需要测试但测的对象有点不一样。除了常规的“脚本能跑通”还要测“模型的调用决策是否稳定”。我给每个技能建了一个测试集包含三类样例正向样例应该调用该技能的请求比如“把扫描版报销单里的金额提出来”应该命中pdf_table_extractor。负向样例不该调用该技能的请求比如“把这段文字翻译成英文”不应该命中pdf_table_extractor。边界样例描述模糊、需要模型判断的请求比如“处理一下这个文件”模型应该追问而非乱选技能。回归测试的逻辑就是改任何一个技能的描述或Schema后跑一遍全量样例看调用决策有没有变化。这个测试我在本地用pytest加一个断言“模型输出的工具名是否符合预期”来完成。操作上我先用脚本记录当前版本对每个样例的决策改动后再跑一遍做diff差异列表一眼就能看出来。这块测试的价值在于它能让你“放心改描述”。没有测试的时候每次改技能描述都提心吊胆怕影响其他场景有回归测试兜底后迭代速度明显加快。8.2 灰度发布与技能版本管理技能库的发布节奏不像业务功能那么严格但它有自己的问题同一个技能在不同项目里可能依赖不同版本的依赖库。我推荐给每个技能加一个requirements.txt并在技能目录里记录“依赖锁定版本”。发布新版本时先让新的技能包在测试项目里跑几天确认没问题再全量同步。因为技能包本质上是“代码文档数据”的混合体我用Git管理并给每个技能打tagv1.0.0、v1.1.0发布时用CI构建成可部署的zip包。主程序只引用技能版本号不直接依赖具体文件内容这样回滚也方便——只需要把版本号指回上一版。另外一个实操经验给技能加一个“状态”字段取值是stable、beta、deprecated。检索时优先返回stable技能只有用户明确要求“使用新功能”时才注入beta技能。这能防止不稳定的新技能拖垮整体体验。9. 最后再分享几个小经验项目做到后面真正决定体验好坏的往往不是模型本身而是“技能资产”的整理功底。我最后想说的是不要一上来就追求技能数量先把手头最常用的5个技能做到高质量比堆100个粗糙技能有用得多。另外技能描述一定要做定期复查。业务术语会变用户说法也会变三个月前写的描述可能已经跟不上实际使用了。我习惯每个季度做一次技能描述审计拿上一季度的真实用户请求回放一遍看哪些技能的命中率下降了及时修订。如果你打算自己搭一套agent-skills大胆试但记住两件事第一技能库的协议设计比实现代码重要先把SKILL.md和schema.json的规范定好再谈功能第二多花时间在测试集上那是你迭代的底气。希望这些踩坑经验能帮你少走几条弯路。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门