ARTICLE DETAIL

资讯详情

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

Agent技能库实战:让大模型真正动手干活的设计与实现

Agent技能库实战:让大模型真正动手干活的设计与实现 最近在调一个 Agent 项目模型聊得头头是道一到实际干活就抓瞎——让它分析一份 CSV它回你一段 Python 代码让你自己跑让它查个网页它说“需要您手动打开浏览器”。后来我把精力从“调提示词”转向“给 Agent 装技能”这才真正解决了问题。这个项目就是我一直在维护的 agent-skills一个面向 AI Agent 的可复用技能库。简单说它不是一套现成的业务代码而是一套“Agent 的能力工具箱”把 Agent 在真实任务中需要的高频能力网页抓取、数据处理、文件解析、代码执行等封装成统一规范的技能模块每个技能自带说明文件、实现代码和依赖清单Agent 按需加载、即插即用。这篇文章会把这套技能库的设计思路、核心模块实现、完整搭建过程和踩坑记录全部拆开讲。如果你是做 Agent 应用开发、或者在折腾私有化 AI 助手想让它真正“动手干活”这篇文章很适合你。我会尽量把每个选择的“为什么”讲清楚也会把那些文档里不会写的坑翻出来念叨一遍。1. 技能库的整体设计与思路拆解1.1 为什么 Agent 不能只有“脑子”没有“手脚”大模型本质上是一个“高智商但没有手脚”的系统。你用自然语言给它下指令它能理解、能规划、能生成文本但真正执行动作——发一个 HTTP 请求、读一个本地文件、运行一段数据分析脚本——它做不到除非有人帮它把“动作”变成“可调用的函数”。早期大家解决这个问题靠两种方式。一种是让模型生成代码用户拿去运行另一种是提前在系统提示词里塞一堆“你有以下工具可用”的描述。这两种方式都有明显短板代码生成方式对模型能力要求高出错后人机交互成本反而更大提示词方式本质上是在压榨上下文长度技能一多提示词爆炸模型反而开始“选择性失明”经常不调用任何工具。skill 库的定位就是充当 Agent 的“标准化工具层”。每个技能的元数据描述模型“什么时候该用我、需要什么参数、返回什么结构”模型只需按格式输出调用请求真正干活的是背后的函数。这样把“思考”和“执行”分开各干各擅长的事。1.2 技能库设计的三条原则这个技能库在设计之初定了三条原则后面所有模块都围绕它们展开一是单一职责。一个技能只负责一件事。web_fetch 只做网页抓取和正文提取不做内容总结data_analysis 只做数据统计与报表输出不负责可视化绘图。这样每个技能的 prompt 描述可以写得很精准模型也更容易在正确场景下选中它。二是声明式接口。每个技能通过一份 SKILL.md 文件对外暴露自己的接口信息包括名称、功能描述、参数定义、返回结构、使用示例。这套元数据既给人看也至关重要地——给模型看。模型在决策时就靠这份描述判断是否需要调用、参数怎么写。三是可插拔。技能之间不互相依赖每个技能是一个独立目录带着自己的 requirements.txt。想加一个技能往 skills 目录下扔一个新文件夹就行想下线直接删掉目录。这保证了技能库可以随项目需求持续生长而不会变成一座动一处就要全量回归的危楼。2. 核心模块的设计与实现细节2.1 技能元数据让模型“看懂”技能技能描述的质量决定了模型调用的准确率。我在 SKILL.md 的 front matter 里定义了这样一组字段name、description、version、author、tags、parameters、returns。其中 description 是最关键的必须说清楚触发场景、能力范围和典型用法。一开始我写过那种很虚的描述比如“用于网页相关操作的工具”结果模型要么不调用要么在明显不合适的场景乱调用。后来总结出一条经验描述里必须包含触发场景的显式信号。比如“当用户提到抓取网页、提取页面内容、查看某个 URL 的正文时使用”这种明确动词和场景词的写法实测下来调用准确率高出一大截。参数定义这块我会给每个参数设置合理的 type、required 和 default并在 description 里补充参数的取值范围。模型虽然不会动态校验参数类型但你把 schema 写得越清楚、默认值给得越合理它生成出来的调用就越规范。还有一个容易忽略的设计returns 字段。别小看这个模型调用技能后需要根据返回结果决定下一步动作。明确告诉它返回的是一个 dict、一段 Markdown 文本还是一张图片路径它才能继续正确编排后续对话。2.2 网页抓取技能 web_fetch这个技能解决 Agent 最常见的刚需用户甩一个链接过来希望 Agent 能读到里面的内容。它的实现不算复杂但有几个细节非常影响稳定性。核心流程是用 requests 带自定义 User-Agent 发起请求检查状态码和响应编码再用 BeautifulSoup 去解析 HTML提取 title 和正文文本。其中编码处理我是吃过亏的——有些老网站响应头里不声明 charset直接r.text会得到一堆乱码所以我会用r.apparent_encoding做兜底修正甚至允许通过参数手动指定编码。另一个稳定性的关键点超时控制。requests 必须显式传入 timeout否则遇到一个响应极慢的服务器整个技能会卡住Agent 的整个任务链都会停滞。我习惯设成(10, 30)分别对应连接超时和读取超时。提示抓取类技能一定要限制返回文本长度。一个超大页面动辄几万字符全塞回给模型既浪费 token 又稀释注意力。默认max_chars5000超出部分做截断并在返回字段里标记truncated: true让模型知道信息不全。2.3 数据分析技能 data_analysis这个技能是让我觉得“Agent 终于有用”的一个转折点。它接收一个文件路径和一段统计需求描述内部用 pandas 读取文件并执行统计最后把结果输出成 Markdown 表格。参数设计上除了file_path和requirements我还加了一个output_language参数控制报表说明文字用中文还是英文。这听起来有点多余但实际用下来非常有用——很多业务场景要求最终报告语言和用户提问语言一致模型调用技能时并不知道应该用哪种语言生成说明所以让功能里直接处理。实现上我有一个小心思统计结果不直接返回 DataFrame而是先转成带格式的 Markdown 字符串。这样模型拿到文本可以直接引用不用再“脑补”数据内容。代码层面比较值得说的是错误处理。用户给的 CSV 可能列名有空格、可能有空值、可能有 NaN。统一在技能内部做清洗和兜底枚举列名去空格、填充或删除缺失行、数值列自动类型推断。这些逻辑放在技能里模型就不用每次在提示词里被反复教育“你要先检查数据质量”。2.4 安全执行技能 code_runner如果说 web_fetch 和 data_analysis 是“给 Agent 装手”code_runner 就是给 Agent 装了一双灵活但需要拴着绳子的手。它让 Agent 可以实际执行一段 Python 代码适用于计算、文本批量处理、动态数据变换等场景。安全上我做了三层防护。第一层代码进入执行前先做 AST 解析检查 import 语句和调用节点命中黑名单文件删除、网络请求、子进程操作等直接拒绝执行。第二层用 subprocess 在独立进程中运行并设置超时防止死循环或资源耗尽。第三层限制输出大小避免海量打印拖垮主进程。这里有个血泪教训一定不要图省事直接eval()或exec()用户传来的代码。模型生成的代码虽然一般没有恶意但它可能因为逻辑缺陷产生意外副作用比如误删文件。现在这个技能上线前我们内部做过一轮加固黑名单里涵盖了 os.remove、shutil.rmtree、socket、subprocess 等高风险操作。3. 实操搭建从零到一构建 agent-skills3.1 目录结构与脚手架初始化一个可用的技能库目录结构不需要花哨但必须整齐。我的实际目录长这样agent-skills/ ├── skills/ │ ├── web_fetch/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ ├── data_analysis/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ └── code_runner/ │ ├── SKILL.md │ ├── main.py │ └── requirements.txt ├── loader/ │ ├── registry.py │ └── validator.py ├── examples/ │ └── minimal_agent.py └── README.md每个技能目录三件套SKILL.md 负责接口描述main.py 负责实现requirements.txt 记录依赖。loader 目录放技能注册器和校验器把“加载技能”这件事也做成标准流程。初始化脚手架时我建议直接写一个 create_skill.sh 脚本自动生成目录和三件套模板避免每次手动复制导致格式不一致。脚本逻辑就三步读技能名参数、创建目录、用 heredoc 写入根模板。3.2 技能注册器让技能库可发现有了技能目录还要有一层逻辑把它们加载进内存我称之为 registry。它的职责是扫描 skills 目录、解析每个 SKILL.md、验证接口完整性、把可调用函数注册到字典中。注册器核心代码如下import yaml from pathlib import Path from importlib import import_module def load_skills(skills_dir: str) - dict: registry {} for skill_path in Path(skills_dir).iterdir(): meta_file skill_path / SKILL.md if not meta_file.exists(): continue with open(meta_file, encodingutf-8) as fp: meta yaml.safe_load(fp) module import_module(fskills.{skill_path.name}.main) registry[meta[name]] { meta: meta, handler: getattr(module, main), } return registry这段代码的重点是约定技能目录名必须与 Python 模块名一致且 main.py 中必须暴露一个main(**params)入口函数。这个约定写进 README以后每个技能作者照着写就行。注册器在 Agent 服务启动时执行一次把全部技能加载到内存避免每次调用都重新读磁盘。加载完技能后还需要把每个技能的 description 和 parameters 拼装成模型可识别的 tools 结构。这一步是连接大模型和技能库的桥梁——模型看到的“工具定义”本质上就是从 SKILL.md 的 fields 字段映射过去的。3.3 与 Agent 主程序对接一个最小可运行示例技能库不是独立运行的它必须嵌入到 Agent 推理循环中。我这里给一个最小可运行的对接逻辑方便你理解整体调用链路import json from loader.registry import load_skills def run_agent_with_skills(user_query: str): skills load_skills(skills) # 组装 tools 描述 tools [ { type: function, function: { name: meta[meta][name], description: meta[meta][description], parameters: { type: object, properties: { p[name]: { type: p[type], description: p[description], } for p in meta[meta][parameters] }, required: [ p[name] for p in meta[meta][parameters] if p.get(required) ], }, }, } for meta in skills.values() ] # 调用模型得到 tool_calls response call_llm(user_query, tools) # 如果模型决定调用技能 if response.tool_calls: for call in response.tool_calls: result skills[call.function.name][handler]( **json.loads(call.function.arguments) ) return result return response.contentcall_llm是你集成的大模型 API 入口所有主流平台都支持 function calling协议大同小异。核心处理逻辑其实就两点一是把注册器的技能信息转换为 tools 结构二是拿到模型的 tool_calls 后按函数名找到对应的 handler 并传入参数执行。调试这个闭环时我建议用最简单的场景练手让用户输入“帮我统计 sales.csv 里每个月的销量总和”看模型是否准确选中 data_analysis 技能、是否正确生成 file_path 参数、返回结果是否符合预期。跑通这一个整个链路就通了后面加技能只是重复劳动。3.4 验证器防止“有问题的技能”上线技能库是会持续增长的人多手杂难免有技能写了个半成品就提交。我写了一个 validator 脚本在 CI 或提交前跑一遍检查三个硬性条件SKILL.md 是否存在且 front matter 完整parameters 中 required 字段是否合法main.py 是否可导入且暴露 main 函数。校验逻辑不复杂但作用很大。上次有个同事提交了一个技能SKILL.md 的 YAML 缩进写错了注册器静默跳过排查了好久才发现是格式问题。有了校验器这类低级错误在源头就被拦住了。validator 还会做一件额外的事检查 requirements.txt 里的依赖是否已经配置在统一环境里。因为技能库依赖会越来越多如果每个技能都随机引入新依赖最后环境会变成一个依赖黑洞。我采用的方式是所有技能共享一个大 requirements.txt 用于生产环境每个技能目录里的 requirements.txt 主要起文档作用。4. 常见问题与排查技巧实录4.1 技能描述写了但模型就是不调用这是技能库落地时最常碰到的问题大概率是 SKILL.md 中的 description 写得不够“场景化”。模型决策调不调用工具主要看当前用户问题与你描述之间的语义相关性。如果你只写“这是一个网页抓取工具”模型很难把它和用户说的“帮我看看这篇文章讲了啥”关联起来。更好的写法是给描述加上触发信号“当用户提供 URL 并要求查看网页内容、提取正文、抓取页面信息时使用”或“当用户需要获取某个链接的文本内容时使用”。我在多个场景下实测同样的功能描述里包含明确触发词后调用率能有显著提升。另外注意description 不宜过长关键信息优先。模型读取工具描述也是注意力有限的把最重要的触发条件放在前 30 个字内比写了一堆背景说明有效得多。4.2 参数永远“不对齐”类型和缺失问题模型生成的参数偶尔会出现类型错误或缺失必填项。深层原因是模型对 JSON Schema 字段类型的理解不够稳定尤其在嵌套结构下。我的实践是在 handler 入口统一加一层参数校验和兜底逻辑。def main(url, max_chars5000): max_chars int(max_chars) if not url or not url.startswith((http://, https://)): raise ValueError(url 参数必须以 http(s):// 开头) # 业务逻辑...这层兜底的目的是即使模型传了怪异的参数值也不会让整个调用链崩溃最多返回一个清晰的错误并让模型根据错误信息自我纠正。顺带提一句错误信息要写得像“给模型看”的包含正确的参数格式示例这样模型下次生成的参数就会正确得多。4.3 技能卡死或超时技能一旦发起网络请求或启动子进程就可能遇到外部服务无响应、网络不通、进程阻塞等问题。我统一规范了超时处理所有 IO 操作显式传 timeout所有外部子进程设置超时上限并在技能主函数最外层包一层 func_timeout 兜底。遇到偶发超时比“拉长超时时间”更有效的办法是增加重试机制。对幂等的请求比如网页抓取失败后自动重试两次间隔递增对于非幂等操作则坚决不重试宁可报错也不重复执行。这条规则对我整个技能库的稳定性提升非常明显。4.4 技能之间的“互相打架”当技能库数量超过一定规模会发现不同技能之间可能因为描述语意重叠导致模型选错工具。比如既有一个 read_csv 技能又有一个 data_analysis 技能用户说“看一下这个表格”模型可能就蒙了。解决思路有两个层面。一是设计阶段尽量收窄每个技能的场景让技能之间边界清晰。二是在 description 里显式写上“不适用场景”比如只读技能的描述末尾加一句“本技能仅读取内容不做统计或分析”。这种负向描述看上去有点反直觉但能大幅减少模型“二选一”的困惑。4.5 技能库的扩展和维护建议按我目前的经验一个稳定的技能库不需要贪多。先把高频、通用的技能做扎实跑通整体链路再根据业务需求逐步扩展。技能越多prompt 中的工具描述越长模型选择和参数生成的准确率反而可能下降。所以我的建议是每个技能都要能打而不是靠数量堆。技能上线前我会用一个包含几十条真实问题的测试集过一遍人工检查每条问题是否命中了正确的技能、返回结果是否可用。这个回归测试集每次新增技能或改动技能描述后都会跑一遍防止改一个技能影响其他技能的调用准确率。提示技能库的“缓存问题”也容易被忽略。Agent 服务常驻运行时技能注册表和依赖库都缓存在内存里。改动技能后务必重启服务或热更新 registry否则你排查了半天以为代码有 bug实际上加载的压根是旧版本。写在最后的一些体会从最初只想解决“让 Agent 动手干活”这个小问题到现在这个技能库已经支撑了多个自动化和数字助手场景最大的感受是AI Agent 的上限不取决于模型多聪明而取决于你给它接了多少高质量的“手脚”。有时候与其花大量时间调提示词、调模型参数不如沉下心来把技能做得更稳定、更精准。最后分享一个小技巧技能库是“给模型用的接口”所有描述和返回都应该站在模型的角度写而不是站在人的角度。你写的时候觉得“这个参数很明显”但模型没有领域常识它只会看你写的 schema。把模型当成一个极度聪明但完全不了解你业务的新同事所有边界、默认值、触发条件都交代清楚它的表现会远超你预期。这个项目的迭代还没停下一步我计划加入更多事件驱动型技能和异步任务支持让 Agent 不仅能即时响应还能执行长时后台任务。如果你也在做类似的技能库欢迎交流这些坑我先替你踩过了。
返回列表