AI Agent技能库设计:从工具调用到模块化工程实践
发布时间:2026/9/24 23:31:49 作者:尧图编辑部 阅读量:1,286

1. 项目定位与核心思路1.1 为什么需要 agent-skills 这类项目做 AI Agent 开发的朋友应该都有体会模型本身的能力再强真落到具体业务里总会遇到几个课本里没教过的场面。你让 Agent 帮你查天气它可能知道调用 API你让它帮你操作一个内部的审批系统它就开始胡编乱造了。根本原因在于大模型的知识和能力是隐式的它知道很多概念但缺少对具体工具、具体操作流程的肌肉记忆。agent-skills 这个项目说白了就是给 Agent 装一套外挂技能库。它把那些模型天生不擅长、但业务里又高频出现的能力——文件解析、网页操作、数据库查询、专业计算、特定业务逻辑——全都封装成独立的、可插拔的技能模块。Agent 运行时按需加载这些技能而不是靠模型临场发挥瞎猜。这个思路在工程上解决了一个很现实的问题Agent 的能力边界不再受限于模型参数大小。你用的可能是 7B 的小模型但只要技能库足够丰富它在特定任务上的表现完全可以逼近甚至超过超大模型在这方面的默认表现。反过来你用了很强的模型也不意味着不需要技能库——强如 GPT 级别的模型让它直接操作一个复杂的 JSON 结构时该出错还是会出错给它一个写好的解析工具它就一次搞定了。1.2 这个项目适合谁能解决什么如果你属于下面几类人这个项目的思路应该能直接帮到你正在做 Agent 原型验证的开发者发现 prompt 怎么调都调不稳功能时好时坏。团队里已经上线了 Agent 应用但每次新增一个工具能力都要改主程序、重新部署迭代效率很低。想做 Agent 能力复用比如多个 Agent 都需要 OCR 识别、表格抽取、PDF 解析这类基础能力不想每个 Agent 单独造轮子。项目核心价值就是三个字解耦——把模型推理和工具执行彻底分开。模型只负责理解意图、做决策规划具体执行交给标准化的技能模块。谁擅长什么就做什么有问题也能快速定位是在想错了还是做错了。2. 整体设计与模块拆解2.1 项目目录结构与职责边界我先说下这套体系的整体架构你如果打算自己搭一套照这个思路走基本不会偏。整个系统分三层接入层负责接收用户请求通常是主 Agent 程序做一些意图识别和任务拆解。调度层这是技能库的心脏负责管理技能注册表、路由分发、参数校验决定某个任务该调用哪个技能、怎么传参。执行层一个个独立的技能实现各有各的输入输出协议互不干扰。对应的项目目录大概长这样agent-skills/ ├── registry.json # 技能注册表 ├── skills/ │ ├── pdf_parser/ │ │ ├── skill.json # 技能描述文件 │ │ ├── main.py # 技能实现 │ │ └── requirements.txt │ ├── sql_query/ │ │ ├── skill.json │ │ └── main.py │ └── web_search/ │ ├── skill.json │ └── main.py └── runtime/ ├── loader.py # 动态加载器 └── dispatcher.py # 调度分发器2.2 技能描述文件的设计要点每个技能模块必须配一份skill.json这是整个体系的接口契约。我见过不少项目死就死在契约不统一——有的技能返回 JSON有的返回纯文本调度器写起来简直想骂人。所以技能描述文件里这几项是必须的{ name: pdf_parser, version: 1.2.0, description: 解析 PDF 文件内容支持提取文本、表格和图片 OCR, author: team_ai, inputs: { type: object, properties: { file_path: {type: string, description: PDF 文件路径}, mode: {type: string, enum: [text, table, ocr], default: text} }, required: [file_path] }, output: { type: object, properties: { status: {type: string, enum: [success, failed]}, data: {type: object}, error_msg: {type: string} } } }这里有个参数设计的门道inputs里的字段一定要用description写清楚语义因为这个描述文件不只是给你看的也是给模型的 prompt 用的。调度器把技能描述拼接进系统提示词里模型看懂了才知道该传什么参数。描述写得含糊模型就会瞎猜这也是很多 Agent 调用工具时参数乱传的根源。2.3 技能注册表如何做到热插拔registry.json是技能库的通讯录调度器启动时读一次把每个技能的描述信息缓存到内存里。它的结构很简单但有一个很关键的字段需要你自己定义好——启用状态和路由权重{ skills: [ { name: pdf_parser, path: skills/pdf_parser, enabled: true, priority: 1 }, { name: web_search, path: skills/web_search, enabled: false, priority: 2 } ] }enabled字段不能省。我试过把不用的技能模块直接删掉结果后面想恢复的时候发现代码已经改得面目全非了。用开关控制比直接删目录稳妥得多也方便线上线下环境用不同配置。priority用于技能重名时的调度优先级如果两个技能都声明自己会翻译那就让优先级高的先上。3. 核心实操手写一个技能模块的完整过程3.1 准备工作先把环境跑通在动手写技能之前先把开发和运行环境理顺。我个人常用的方案是 Python 3.10 搭配 FastAPI 做技能的服务框架因为技能之间需要通信HTTP 是最省事的协议。但如果你只想做本地原型直接用 Python 函数式注册就够了不需要起服务。下面是我常用的技能模块骨架# skills/pdf_parser/main.py import json from typing import Any, Dict def run(inputs: Dict[str, Any]) - Dict[str, Any]: 技能入口函数所有技能必须实现此函数 try: file_path inputs.get(file_path) mode inputs.get(mode, text) if not file_path: return {status: failed, error_msg: file_path is required} # 具体业务逻辑在这里 result parse_pdf(file_path, mode) return { status: success, data: result } except Exception as e: return { status: failed, error_msg: str(e) } def parse_pdf(file_path: str, mode: str) - Dict[str, Any]: # 假设用 pdfplumber paddleocr # 代码省略具体实现按你的业务需求来 pass这里有个容易踩的坑技能函数最好用统一的异常捕获把所有错误都转成标准化的error_msg返回。有些开发者在技能里不加异常处理直接让异常抛到调度器结果 Agent 收到的是一堆看不懂的 traceback模型完全不知道该怎么往下走。技能函数是给系统用的不是给开发者调试用的输出必须稳定可控。3.2 多技能调用的路由编排单个技能写好之后下一步是让调度器能根据用户请求自动选择该调谁。我实现了一个简单的 dispatcher核心逻辑不复杂# runtime/dispatcher.py import json from typing import List, Dict, Any class SkillDispatcher: def __init__(self, registry_path: str): with open(registry_path, r, encodingutf-8) as f: self.registry json.load(f) self.skill_cache {} def load_skill(self, name: str): 按需加载技能模块 if name in self.skill_cache: return self.skill_cache[name] for skill_info in self.registry[skills]: if skill_info[name] name and skill_info[enabled]: module_path skill_info[path].replace(/, .) module importlib.import_module(f{module_path}.main) self.skill_cache[name] module.run return module.run raise ValueError(fSkill {name} not found or disabled) def execute(self, skill_name: str, params: Dict[str, Any]) - Dict[str, Any]: 执行技能并统一结果格式 skill_func self.load_skill(skill_name) result skill_func(params) # 这里可以做结果后处理 return result这种按需加载的方式有个好处内存里只保留被真实调用过的技能agent 系统跑久了也不会越吃越多。运行时加载到内存的模块在self.skill_cache里缓存住重复调用不用重新导入性能上不吃亏。3.3 模型提示词与技能描述怎么联动调度器只是路由器真正让 Agent 学会调用技能的是系统提示词里对技能清单的呈现方式。我的经验是不要把所有技能的 description 原封不动都塞进去应该做一次裁剪只把可能相关的技能放给模型看。比如用户问上周的销售数据怎么样这时候你不应该把 PDF 解析技能的 description 喂给模型它只应该看到sql_query和data_analysis这类相关技能。怎么做这个预筛一种朴素有效的方式是关键词匹配加语义检索# 预筛伪代码 def filter_skills(query: str, skill_descriptions: List[Dict]) - List[Dict]: # 用简单的关键词打分或者用 embedding 向量召回 # 返回与 query 相关的技能列表 pass把这个预筛环节加上之后Agent 的意图识别准确率提升了不止一个档次。模型不用在一堆无关技能里挑挑拣拣自然不容易选错。4. 避坑实录与常见问题速查4.1 技能加载失败与路径混乱开发中最常遇到的问题就是技能模块导入出错尤其是当你用相对路径导入时技能模块一旦嵌套层级变多importlib的搜索路径就会出问题。我的建议是项目启动时就在入口模块里把项目根目录写进sys.path后面所有动态导入都用绝对路径避免反复折腾相对路径。另一个初学者容易踩的坑技能模块里依赖的第三方库版本冲突。比如pdf_parser依赖pdfplumber的旧版本而这个旧版本跟主程序用的另一个库不兼容。解决办法是每个技能单独建虚拟环境技能通过进程间通信调用而不是全部跑在一个解释器里。如果嫌部署复杂退而求其次也要用 Docker 容器给技能做隔离。4.2 模型调技能时参数传得不对这类问题很典型模型看了技能描述之后仍然传错参数或者以错误的格式构造参数。可能的原因有三类技能描述写得不清晰没有把参数约束说明白。模型上下文被截断技能描述没完整喂进去。系统提示词里缺少调用的规范示例。针对最后一个原因我的做法是在系统提示词里加一个技能调用示例当用户需要执行某个技能时请按以下格式输出 {skill: 技能名, params: {参数1: 值1, 参数2: 值2}} 比如用户说帮我查一下这个目录下的PDF文件里写了什么 输出{skill: pdf_parser, params: {file_path: /tmp/report.pdf, mode: text}}加了这个示例之后模型传参的错误率明显下降。很多人以为模型会自己推理出正确格式但实测下来给它一个标准范式是最省力的。4.3 技能执行超时与假死恢复技能模块运行时间一旦超过 Agent 的响应时限整个请求就会卡住。这种超时问题不能靠模型自己恢复必须从框架层面做兜底。我在调度器里加了两个参数timeout和retry_count。def execute_with_timeout(skill_func, params, timeout30): with ThreadPoolExecutor(max_workers1) as executor: future executor.submit(skill_func, params) try: result future.result(timeouttimeout) return result except TimeoutError: return {status: failed, error_msg: fskill execution timeout after {timeout}s}超时返回一个标准错误Agent 就可以依据错误信息向用户解释处理超时请重试或者走别的替代路径。如果没有这层兜底超时导致的 session 挂起排查起来极其痛苦。4.4 技能更新后不生效怎么办改完技能代码发现调用的还是旧结果十有八九是缓存问题。调度器里的skill_cache缓存了模块引用代码改了并不会自动重载。最简单粗暴的解决方式是在开发环境关闭缓存每次请求重新 import。但生产环境不建议这么做因为性能损失比较大。更好的方案是在注册表里加一个version字段技能文件更新时同步修改版本号调度器检测到版本变化就主动清理缓存重载。另外建议技能目录里加一个CHANGELOG.md每次改动都写上变更内容。这虽然是个不起眼的习惯但等技能数量多到二三十个的时候你会发现没有变更记录根本记不清哪个版本改了什么。4.5 常见问题速查表问题现象可能原因解决思路技能模块导入报错路径配置不对或依赖缺失检查sys.path确认requirements.txt已安装模型总是选错技能技能描述语焉不详或预筛环节把相关技能过滤掉了重写 description调整预筛关键词/阈值技能执行结果不稳定参数传入不规范技能内部对脏数据没做校验在技能入口处加参数校验强制类型转换新增技能后原有技能不可用注册表格式错误或技能间存在依赖冲突用 JSON 校验工具检查注册表逐个技能隔离排查线上技能更新后没生效模块缓存未刷新检查版本号机制重启调度器或强制清理缓存多个技能并发调用互相阻塞技能实现里有全局锁或共享状态技能模块尽量无状态化共享资源用线程锁管理5. 更进一步的扩展思路5.1 技能市场与跨团队共享技能库的方案跑通之后很自然的进化方向就是技能市场。团队内部把高频技能沉淀出来其他项目可以像装插件一样拉过来直接用。做到这一步需要给技能加一个规范的发布流程技能代码通过 CI 跑一遍单元测试和冒烟测试测试通过后打版本号上传到内部的制品仓库使用方通过 registry 配置依赖某个版本的技能这一步做扎实Agent 开发效率能提升一个量级。别的不说光是一个稳定可复用的 PDF 解析技能就能让全公司所有项目少踩一遍 PDF 格式的坑。5.2 技能编排与多技能协同单技能能解决单点问题但真实业务往往是多技能协同。比如用户问把这份合同里的付款条款抽出来然后和上个月的标准条款做个对比这需要依次调用pdf_parser、doc_compare、data_analysis三个技能。我的建议是不要把编排逻辑写在主程序里而是定义一个技能流水线描述让调度器按顺序执行{ pipeline: [ {skill: pdf_parser, params: {file_path: $input.file_path, mode: text}}, {skill: clause_extractor, params: {text: $prev.data.text}}, {skill: doc_compare, params: {content: $prev.data.clauses}} ] }中间用$prev.data引用前一步的输出调度器按 DAG 顺序执行。这套机制加上去之后Agent 能处理的场景复杂度瞬间跃升而不是每新增一个业务场景就要写死一段代码。5.3 技能的自修复与自进化最后聊一个比较超前的想法技能执行失败时能不能让 Agent 自己修一下再重试比如pdf_parser失败是因为缺了一个解码库调度器能不能先检查环境依赖自动安装缺失的库然后再跑一次这个方向技术上完全可行但操作上要非常慎重因为自动安装依赖可能带来安全风险技能内部代码也未必对异常场景做了完善的恢复处理。作为实验项目可以尝试生产环境还是老老实实做好监控和告警让 AI 自动改代码这件事离真正安全可控还有相当距离。我在实际使用中发现技能库的维护是个持续投入的活跟写代码不同它更像是在经营一座图书馆——你需要不断收录新书、修订旧书、淘汰没人看的书。只要你把技能的定义、加载、调度这一套基础打扎实了后面往上面加内容是非常顺畅的。最后分享一个我自己的小习惯每次新写一个技能时我会先用一个 3~5 次的真实用户对话场景去测试它而不是只跑单元测试。因为技能最终是要被模型的 prompt 调用的模型生成的参数千奇百怪只有真实场景才能暴露参数适配和异常处理的问题。等这些场景都跑顺了再写单元测试固化下来这个技能才算真正毕业。