AI智能体技能库:模块化设计、核心技能实现与工程实践
1. 项目概述一个面向AI智能体的技能库最近在折腾AI智能体Agent开发发现一个挺有意思的现象很多开发者包括我自己在内一开始都热衷于研究各种大模型LLM的API调用和Prompt工程但真到了要让智能体去“做事”的时候比如让它去分析一个网页、处理一份PDF、或者调用某个第三方服务就有点抓瞎了。这感觉就像你给一个员工配了一台顶配电脑但他只会用记事本Word、Excel、浏览器这些“办公技能”一个都不会。oleg-koval/agent-skills这个项目在我看来就是为解决这个问题而生的。它不是一个完整的智能体框架而是一个技能Skills库。你可以把它理解为一个“工具箱”或者“插件市场”里面预先封装好了各种AI智能体在实际工作中可能需要用到的具体能力。比如从网页抓取并总结内容、读取和分析PDF文档、与数据库交互、调用搜索引擎、处理Excel表格等等。它的核心价值在于让开发者能够像搭积木一样快速为你的智能体赋予各种实用技能而无需从零开始编写每一块功能。这个项目特别适合两类人一是正在构建复杂AI智能体应用的开发者尤其是那些需要智能体与外部世界文件、网络、API进行交互的场景二是对AI应用层开发感兴趣想了解如何将大语言模型的“思考”能力转化为具体“行动”的学习者。通过拆解和使用这个技能库你能更清晰地理解一个功能完备的智能体是如何被组装起来的。2. 核心设计思路技能即插即用的模块化哲学2.1 为什么需要“技能”抽象在传统的软件开发中我们通过编写函数、类和方法来封装功能。但在基于大语言模型的智能体系统中情况有些不同。智能体的核心是一个“大脑”LLM它负责理解和规划但它本身不具备“手”和“脚”去执行具体任务。我们需要一种方式来告诉这个大脑“这里有一些工具你可以用这是工具的用途、使用方法和注意事项。”这就是“技能”概念的由来。一个技能本质上是一个标准化、可描述、可调用的功能单元。它通常包含几个关键部分功能描述用自然语言清晰说明这个技能是做什么的比如“从给定的URL获取网页内容并提取正文”。输入/输出规范明确技能需要什么参数如URL地址以及会返回什么格式的结果如纯文本、JSON。执行逻辑背后真正执行任务的代码可能是调用一个网络请求库、解析一个文件或者访问一个API。元数据例如技能的名称、分类、所需权限等便于管理和检索。agent-skills项目采纳的正是这种模块化哲学。它没有尝试去发明一个新的智能体框架而是专注于提供高质量、开箱即用的技能实现。这种设计的好处是解耦和复用性极强。你的智能体大脑无论是使用LangChain、LlamaIndex、AutoGen还是自定义框架只需要具备调用技能的能力就可以轻松接入这个庞大的技能生态。2.2 技能库的架构与组织方式浏览agent-skills的代码仓库你会发现它的结构非常清晰通常按技能的功能领域进行组织。这是一种非常实用的分类方式让开发者能快速找到所需。skills/ ├── web/ # 网络相关技能 │ ├── scrape_web.py # 网页抓取 │ └── search_web.py # 网络搜索 ├── document/ # 文档处理技能 │ ├── read_pdf.py │ ├── read_docx.py │ └── summarize_text.py ├── data/ # 数据操作技能 │ ├── query_sql.py │ └── process_csv.py ├── code/ # 代码相关技能 │ └── run_python.py └── system/ # 系统交互技能 └── execute_command.py每个技能文件都是一个独立的模块。以web/scrape_web.py为例其内部实现通常会遵循一个模式定义技能函数scrape_web(url: str) - str。在函数内部使用如requests、BeautifulSoup或playwright等库来获取和解析网页。对获取的内容进行清理和提取去除广告、导航栏等噪音保留核心正文。返回纯文本或结构化的摘要信息。更重要的是项目往往会为每个技能提供一个详细的描述性Prompt。这个描述不是给机器看的注释而是给大语言模型看的“工具说明书”。例如技能名称:scrape_web描述: 根据用户提供的URL抓取该网页的HTML内容使用智能提取算法获取页面的核心正文文本并返回清理后的纯文本。适用于获取新闻文章、博客帖子、文档页面等内容。输入参数:url(字符串有效的HTTP/HTTPS网址)输出: 字符串包含网页的正文内容。注意事项: 对于需要JavaScript渲染的复杂单页应用(SPA)此技能可能无法获取完整内容。请确保URL可公开访问。这个描述会被整合到给大模型的系统提示System Prompt或函数调用Function Calling描述中从而让LLM知道在什么情况下应该调用这个技能以及如何调用。2.3 与主流智能体框架的集成agent-skills的另一个巧妙之处在于它的“框架中立性”。由于技能被实现为简单的Python函数或类方法它可以相对容易地集成到任何主流的智能体框架中。LangChain: 你可以利用Tool类将技能函数包装成LangChain的工具然后提供给AgentExecutor。LlamaIndex: 可以将技能作为QueryEngineTool或自定义的BaseTool接入。AutoGen: 技能可以作为AssistantAgent的function_map的一部分。自定义框架: 如果你是自己从头构建智能体循环那么只需要在循环中当LLM的输出表明需要调用某个技能时去查找并执行对应的函数即可。这种低耦合的设计使得agent-skills可以作为一个基础能力层被灵活地运用到各种技术栈中极大地扩展了其应用范围。3. 核心技能深度解析与实操要点一个技能库的价值不仅在于它提供了多少技能更在于每个技能实现的鲁棒性、实用性和安全性。下面我们深入剖析几个典型技能看看在实操中需要注意什么。3.1 网页抓取技能不仅仅是requests.get网页抓取 (scrape_web) 可能是最常用的技能之一。一个 naive 的实现可能就是requests.get(url).text但这在实际中会遇到大量问题。核心实现要点请求头模拟必须设置合理的User-Agent等请求头模拟真实浏览器访问否则极易被网站屏蔽。headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, }错误处理与重试网络请求充满不确定性。必须加入超时控制、状态码检查、以及指数退避的重试机制。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retries Retry(total3, backoff_factor1, status_forcelist[502, 503, 504]) session.mount(http://, HTTPAdapter(max_retriesretries)) session.mount(https://, HTTPAdapter(max_retriesretries))内容提取直接返回整个HTML是毫无用处的。需要使用像readability、newspaper3k或trafilatura这类专门用于提取正文的库。它们能有效去除页眉、页脚、广告、评论等噪音。import trafilatura downloaded session.get(url, headersheaders, timeout10) html_content downloaded.text # 使用trafilatura提取核心正文 text_content trafilatura.extract(html_content)处理JavaScript渲染对于现代前端框架如React, Vue构建的网站上述方法只能拿到一个空壳。这时需要引入无头浏览器如playwright或selenium。注意无头浏览器资源消耗大、速度慢应作为备选方案仅在检测到常规方法提取内容为空或极少时触发。实操心得设置速率限制如果你构建的智能体可能高频调用此技能务必在技能内部或调用层面添加速率限制如每秒钟/每分钟最多请求数次避免对目标网站造成压力这也是网络礼仪。缓存策略对于相对静态的内容如文档、新闻可以考虑引入简单的缓存机制例如使用diskcache或redis将URL及其提取内容缓存一段时间。这能大幅提升智能体对相同信息源的响应速度并减少不必要的网络请求。内容长度控制大模型有上下文长度限制。提取的网页正文可能非常长。一个优秀的技能应该提供摘要选项或者自动将过长的内容进行分段处理并在返回时给出提示。3.2 文档处理技能格式解析与信息抽取让智能体读懂PDF、Word、Excel是另一个强需求。这里的难点在于格式的多样性和内容提取的准确性。PDF处理要点工具选型PyPDF2/pdfplumber适合提取文本和简单表格pdf2imagepytesseract用于OCR识别扫描件camelot/tabula专门用于复杂表格提取。agent-skills中的技能可能需要根据PDF类型动态选择策略。元信息获取除了正文还应提取作者、标题、创建日期等元信息这些对于智能体理解文档背景很有帮助。分页与结构保留页码信息或章节标题结构有助于后续的引用和定位。Word/Excel处理要点对于.docx使用python-docx库可以很好地保留段落、列表、表格等结构。对于.xlsx使用pandas的read_excel是最佳选择。技能需要处理可能存在的多个工作表并以一种清晰的方式如JSON或Markdown表格将数据结构化返回给LLM。实操心得统一输出格式无论输入是PDF、Word还是网页经过文档处理技能后输出应尽量统一为干净的、结构化的纯文本或Markdown格式。这能极大简化下游LLM的处理逻辑。处理加密文档技能应能优雅地处理加密或受密码保护的文档返回明确的错误信息如“该PDF文件受密码保护无法读取”而不是让整个智能体进程崩溃。文件大小预警处理一个100MB的PDF和1MB的PDF是完全不同的概念。技能应在开始解析前检查文件大小如果过大可以提前返回警告或询问用户是否继续。3.3 代码执行技能能力与安全的平衡run_python或execute_command这类技能非常强大它让智能体具备了“动手”改变环境的能力但也是最危险的技能。必须极其谨慎地设计和部署。安全设计核心沙箱环境绝对不能在宿主机器上直接执行任意代码。必须使用 Docker 容器、pysandbox已废弃需寻找替代方案或安全的子进程隔离技术如seccomp将代码执行限制在一个资源受限、网络隔离的环境中。超时控制必须为每次执行设置严格的超时如30秒防止无限循环或长时间计算占用资源。资源限制限制内存、CPU使用率和磁盘写入。禁用危险模块/命令黑名单或白名单机制。例如禁止导入os,sys,subprocess或只允许执行少数几个白名单命令。输入净化对用户输入或LLM生成的代码进行基本的恶意模式检查。一个相对安全的run_python技能实现框架import docker import tempfile def run_python_code(code: str, timeout_seconds30): 在Docker容器中执行Python代码。 client docker.from_env() # 使用一个预先构建好的、 stripped-down 的 Python 镜像 container client.containers.run( python:3.9-slim, commandfpython -c \{code.replace(\, \\\)}\, mem_limit100m, # 内存限制 cpuset_cpus0.5, # CPU限制 network_disabledTrue, # 禁用网络 detachTrue, stdoutTrue, stderrTrue ) try: # 等待执行完成并设置超时 result container.wait(timeouttimeout_seconds) logs container.logs(stdoutTrue, stderrTrue).decode(utf-8) container.remove(forceTrue) # 清理容器 return {exit_code: result[StatusCode], output: logs} except Exception as e: container.remove(forceTrue) return {error: fExecution failed: {str(e)}}实操心得永远假设输入是恶意的这是设计此类技能的第一原则。提供清晰的执行上下文技能执行前可以自动在代码前注入一些常用的、安全的导入如math,json,datetime并提供一个虚拟的文件系统路径供其使用。结果过滤对于执行结果特别是错误信息可能包含容器内部路径等无关信息需要进行过滤和格式化使其对用户和LLM更友好。审计日志所有代码执行请求无论成功与否都必须记录详细的审计日志包括代码内容、执行者、时间戳和结果以备追溯。4. 如何将技能库集成到你的智能体项目中理解了核心技能后下一步就是将其“装配”到你的智能体大脑上。这里我们以一个基于 OpenAI Function Calling 和简单循环的自定义智能体为例演示集成过程。4.1 技能加载与描述生成首先我们需要动态加载agent-skills中的技能并为其生成LLM可理解的函数描述。import importlib.util import os from typing import Dict, Any, Callable # 假设技能库路径 SKILLS_DIR ./agent-skills/skills def load_skills_from_dir(skills_dir: str) - Dict[str, Dict[str, Any]]: 从指定目录加载所有技能。 返回格式{‘skill_name’: {‘function’: callable, ‘description’: dict}} skills {} for root, dirs, files in os.walk(skills_dir): for file in files: if file.endswith(.py) and not file.startswith(_): module_name file[:-3] module_path os.path.join(root, file) # 动态导入模块 spec importlib.util.spec_from_file_location(module_name, module_path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # 假设每个模块都有一个 main 函数和 get_description 函数 if hasattr(module, main) and callable(module.main): skill_func module.main skill_desc module.get_description() if hasattr(module, get_description) else {} skill_key skill_desc.get(name, module_name) skills[skill_key] { function: skill_func, description: skill_desc # 包含name, description, parameters等 } return skills # 加载所有技能 available_skills load_skills_from_dir(SKILLS_DIR) print(fLoaded {len(available_skills)} skills: {list(available_skills.keys())})4.2 构建智能体循环接下来我们构建一个简单的智能体循环。这个循环会将用户问题和技能描述一起发送给LLM。LLM判断是否需要调用技能以及调用哪个技能、传入什么参数。系统执行对应的技能函数。将技能执行结果返回给LLM让它生成最终回答。import openai import json # 初始化OpenAI客户端 client openai.OpenAI(api_keyyour-api-key) def run_agent_conversation(user_query: str, skills: Dict, modelgpt-4): 运行一个简单的智能体对话循环。 messages [ {role: system, content: 你是一个有帮助的AI助手可以使用以下工具来回答问题。请根据用户问题决定是否需要使用工具以及使用哪个工具。}, {role: user, content: user_query} ] # 将技能描述转换为OpenAI函数调用格式 functions [] for skill_name, skill_info in skills.items(): desc skill_info[description] functions.append({ name: skill_name, description: desc.get(description, ), parameters: desc.get(parameters, {type: object, properties: {}}) }) # 第一步LLM决定是否调用函数 response client.chat.completions.create( modelmodel, messagesmessages, functionsfunctions, function_callauto, ) response_message response.choices[0].message # 检查是否需要调用函数 if response_message.function_call: function_name response_message.function_call.name function_args json.loads(response_message.function_call.arguments) print(f智能体决定调用函数: {function_name}, 参数: {function_args}) # 找到对应的技能函数并执行 if function_name in skills: skill_to_call skills[function_name][function] try: # 执行技能 function_response skill_to_call(**function_args) # 将结果格式化为字符串 if isinstance(function_response, dict): function_response_str json.dumps(function_response, ensure_asciiFalse) else: function_response_str str(function_response) except Exception as e: function_response_str fError executing skill {function_name}: {str(e)} print(f技能执行结果: {function_response_str[:200]}...) # 打印前200字符 # 第二步将函数执行结果返回给LLM让它生成最终回答 messages.append(response_message) # 添加助手的函数调用消息 messages.append({ role: function, name: function_name, content: function_response_str, }) second_response client.chat.completions.create( modelmodel, messagesmessages, ) final_reply second_response.choices[0].message.content return final_reply else: return f错误请求的技能 {function_name} 不存在。 else: # LLM直接回答了问题 return response_message.content # 示例使用智能体查询网页并总结 user_question 请帮我查看 OpenAI 官网博客的最新文章标题是什么 final_answer run_agent_conversation(user_question, available_skills) print(智能体最终回答, final_answer)在这个循环中available_skills字典里的每个技能都通过get_description()方法提供了标准的函数描述。当LLM决定调用scrape_web时它会生成一个包含url参数的调用请求然后我们的程序会找到对应的scrape_web.main(url)函数并执行将抓取到的网页内容返回给LLMLLM再从中提取出最新文章的标题最终生成给用户的自然语言回复。4.3 多技能协作与规划更复杂的智能体可能需要连续调用多个技能。例如用户问“分析一下特斯拉最近一个季度的财报总结其主要财务数据和市场反应。” 这个任务可能涉及调用search_web技能搜索“Tesla Q3 2024 earnings report”。从搜索结果中选取最相关的链接调用scrape_web抓取财报页面。调用read_pdf技能如果财报是PDF格式。调用summarize_text技能对抓取的长文本进行摘要。再次调用search_web搜索“Tesla earnings market reaction news”。最后LLM综合所有技能返回的信息生成一份完整的分析报告。要实现这种多步规划就需要更高级的智能体架构如ReAct模式或者依赖LLM本身强大的规划能力如GPT-4。agent-skills作为底层技能提供者为这种复杂规划提供了可靠的动作执行保障。5. 常见问题、排查技巧与进阶思考在实际集成和使用agent-skills这类项目时你会遇到一些典型问题。以下是一些实录和解决方案。5.1 技能调用失败排查清单问题现象可能原因排查步骤与解决方案LLM不调用技能直接回答1. 技能描述不清晰。2. 系统提示词未引导使用工具。3. 问题太简单LLM认为无需调用。1. 检查技能的description字段是否准确、具体地描述了功能和适用场景。2. 强化系统提示词例如“你必须使用提供的工具来获取信息不能仅凭已有知识猜测。”3. 在测试时使用明确需要外部信息的复杂问题。LLM调用了错误的技能或参数1. 技能名称或参数描述易混淆。2. 函数描述中的parametersJSON Schema定义有误。1. 技能名称应唯一且具描述性如scrape_web而非get_data。参数名也应清晰如url而非input。2. 仔细核对函数描述中的type,properties,required字段是否符合OpenAI函数调用规范。技能执行超时或报错1. 网络问题或目标服务不可用。2. 技能代码内部异常未处理。3. 输入参数格式错误。1. 在技能函数内部增加更完善的错误处理、重试和超时逻辑。2. 查看技能执行时打印的详细日志或错误堆栈。3. 在执行前对输入参数进行验证和清洗。技能返回结果LLM无法理解1. 返回格式过于复杂或非结构化。2. 返回内容包含大量无关噪音或乱码。1. 尽量使技能返回纯文本、Markdown或标准JSON。2. 在技能内部对结果进行清洗和格式化例如网页抓取技能应过滤掉脚本、样式标签。5.2 性能优化与成本控制当你的智能体开始处理大量请求时性能和成本就成为关键考量。技能执行异步化对于I/O密集型技能如网络请求、文件读取应将其改造成异步函数使用asyncio和aiohttp并在智能体循环中使用异步调用。这可以避免在等待一个技能响应时阻塞整个智能体大幅提升吞吐量。技能结果缓存如前所述对网页内容、API查询结果等实施缓存。可以基于输入参数如URL、查询语句生成一个哈希键将结果缓存一段时间TTL。这不仅能加快响应还能减少对第三方服务的调用次数节约成本。LLM上下文管理技能返回的内容可能很长。直接塞入上下文会消耗大量Token增加成本并可能超出限制。解决方案包括技能端摘要在技能内部集成一个“轻量级摘要”功能或者提供一个summary_only参数让LLM决定是获取全文还是摘要。智能体端压缩在将技能结果放入消息历史前先让LLM例如一个更小、更便宜的模型对其进行关键信息提取和压缩。技能按需加载不必在启动时加载所有技能。可以根据技能的使用频率或分类进行懒加载。例如只有用户问题涉及文档处理时才加载read_pdf等模块。5.3 安全与权限管理进阶在个人项目或封闭环境中安全可能不是首要问题。但一旦部署为公共服务就必须建立严格的权限体系。技能级别的访问控制为每个技能定义一个“风险等级”如低风险-文本处理中风险-网络访问高风险-代码执行、系统命令。为每个用户或API密钥分配一个“权限等级”。在智能体决定调用技能前先检查调用者是否有该技能的调用权限。输入输出审查与过滤输入对用户原始输入和LLM生成的技能调用参数进行审查。例如检查scrape_web的URL是否指向内部网络如192.168.*.*,10.*.*.*防止SSRF攻击。检查execute_command的参数是否包含危险命令。输出对技能返回的内容进行过滤防止敏感信息泄露如技能错误信息中暴露服务器路径或恶意内容如抓取的网页包含有害代码。审计与监控记录每一次技能调用的详细信息时间戳、用户ID、技能名称、输入参数、执行结果或错误、耗时、Token消耗等。这不仅是安全审计的需要也是分析技能使用情况、优化性能和数据的重要依据。5.4 自定义技能开发指南agent-skills提供的技能是通用的但你的项目一定有特殊需求。这时就需要开发自定义技能。开发一个高质量自定义技能的步骤明确契约首先想清楚这个技能是干什么的输入是什么参数名、类型、是否必需输出是什么字符串、JSON、文件用一段清晰的文字描述出来这段文字将来就是给LLM看的“说明书”。实现功能编写健壮的Python函数。遵循单一职责原则一个技能只做一件事。做好错误处理、参数验证、日志记录。编写描述创建一个与函数同名的描述函数如get_scrape_web_description返回符合OpenAI函数调用格式的字典。确保描述准确无误。测试不仅要测试功能是否正确还要测试LLM是否能正确理解描述并调用它。可以手动模拟LLM的调用过程。集成将你的技能模块放入技能目录确保它能被动态加载函数正确识别。例如为你的内部系统开发一个query_customer_db技能# query_customer_db.py import sqlite3 from typing import Dict, Any def main(customer_id: int) - Dict[str, Any]: 根据客户ID查询客户基本信息。 if not isinstance(customer_id, int) or customer_id 0: return {error: Invalid customer_id. Must be a positive integer.} try: conn sqlite3.connect(your_database.db) cursor conn.cursor() cursor.execute(SELECT name, email, join_date FROM customers WHERE id ?, (customer_id,)) row cursor.fetchone() conn.close() if row: return {customer_id: customer_id, name: row[0], email: row[1], join_date: row[2]} else: return {customer_id: customer_id, message: Customer not found.} except Exception as e: return {error: fDatabase query failed: {str(e)}} def get_description() - Dict[str, Any]: return { name: query_customer_db, description: 根据提供的客户ID从内部客户数据库中查询该客户的基本信息包括姓名、邮箱和注册日期。, parameters: { type: object, properties: { customer_id: { type: integer, description: 需要查询的客户的唯一标识ID。 } }, required: [customer_id] } }通过这种方式你可以不断扩展智能体的能力边界使其真正融入你的业务工作流。oleg-koval/agent-skills项目为我们提供了一个优秀的范本它揭示了构建实用AI智能体的一个关键路径将复杂能力分解为可复用、可描述、可安全执行的技能模块。在实际使用中你很少会原封不动地使用它更多的是借鉴其设计思想然后根据自身的技术栈和业务需求构建或集成属于自己的技能库。这个过程本身就是对智能体系统从认知到实践的一次深度提升。当你亲手将一个一个技能“装配”到你的智能体上并看着它开始熟练地使用这些工具解决问题时那种感觉就像在教会一个数字生命如何与世界互动一样充满了挑战与乐趣。