ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenClaw自定义Agent工具开发指南:从原理到实战

OpenClaw自定义Agent工具开发指南:从原理到实战 1. 从“能用”到“好用”为什么需要自定义Agent工具最近在折腾OpenClaw想让它帮我处理一些更具体的任务比如自动整理项目文档、监控特定API状态或者根据代码变更自动生成测试用例。用了一段时间官方自带的工具集后我发现了一个普遍问题通用工具在特定场景下总是差那么点意思。比如让它去分析一个私有Git仓库的提交记录它可能知道怎么调用Git命令但无法理解我们团队自定义的提交规范自然也就做不出符合我们需求的周报。这其实就是所有AI Agent平台都会遇到的瓶颈。平台提供的工具是“最大公约数”保证了基础功能的可用性。但真正的生产力往往藏在那些与你的业务逻辑、技术栈、团队习惯深度绑定的“脏活累活”里。OpenClaw的插件机制特别是其Agent工具开发能力就是为了解决这个“最后一公里”的问题而设计的。它允许你将任何一段代码、一个脚本、一个内部API封装成一个标准的“工具”然后像搭积木一样让AI Agent去调用它。简单来说开发一个OpenClaw Agent工具就是教AI学会使用你的“独家兵器”。这个兵器可能是一个内部的数据清洗脚本、一个连接了公司CRM系统的查询接口或者一个专门用来生成某种特定格式图表的函数。当AI掌握了这些工具它就不再是一个只会聊天的模型而是一个能真正进入你工作流、替你执行复杂操作的智能体。2. 解剖一个OpenClaw Agent工具核心组件与工作原理在动手写代码之前我们必须先搞清楚OpenClaw期望一个工具长什么样。这就像你要给一个新员工培训首先得告诉他公司的汇报流程和文档格式。OpenClaw对工具的定义核心围绕几个部分展开工具的描述、输入参数、执行逻辑以及返回结果的处理。2.1 工具的描述层让AI理解你的工具这是最重要的一步直接决定了AI能否正确、安全地使用你的工具。描述层主要包括name、description和parameters。name: 工具的名称。最好使用动词开头清晰表明动作例如fetch_project_issues、calculate_code_coverage。避免使用模糊的名词。description: 工具的详细描述。这里需要你用自然语言清晰地告诉AI两件事第一这个工具是干什么的第二在什么情况下应该使用它。例如“该工具用于查询Jira项目中状态为‘进行中’的任务。当用户需要了解当前迭代的工作进度时可使用此工具。”parameters: 定义工具所需的输入参数。这是一个JSON Schema对象。你需要为每个参数定义type: 参数类型如string,integer,boolean。description: 参数的描述告诉AI这个参数需要传入什么内容。例如project_key参数的描述可以是“Jira项目的唯一标识键值例如 ‘PROJ’。”required: 是否为必填参数。一个描述清晰的工具AI在规划任务链时就能做出更准确的判断。我曾见过一个描述为“处理数据”的工具结果AI在需要“发送邮件”的时候也调用了它就是因为描述太宽泛。2.2 工具的执行层实现功能的核心描述层告诉AI“用什么”和“怎么传参”执行层则是“具体怎么做”。在OpenClaw中这通常是一个Python函数或类方法。这个函数需要接收描述层中定义的参数执行具体的业务逻辑并返回一个结果。返回的结果最好是结构化的数据如字典、列表或者至少是格式清晰的字符串。因为AI需要解析这个结果并将其用于后续的推理或回答生成。如果返回的是庞大的二进制数据或者复杂的对象AI可能无法处理。# 一个简单的工具函数示例 def search_internal_wiki(query: str, max_results: int 5) - dict: 根据查询词搜索内部Wiki文档。 Args: query: 搜索关键词。 max_results: 返回的最大结果数量默认为5。 Returns: 一个字典包含 ‘success’ 标志和 ‘results’ 列表。 # 这里是具体的搜索逻辑可能是调用ES接口、查询数据库等 # ... fake_results [ {title: OpenClaw部署指南, url: ..., snippet: ...}, {title: API设计规范, url: ..., snippet: ...}, ] return { success: True, results: fake_results[:max_results] }2.3 工具的注册与集成让OpenClaw认识它工具函数写好后需要“注册”到OpenClaw的插件系统中。OpenClaw的插件架构允许你以相对标准化的方式打包和集成工具。通常你需要创建一个插件类在初始化方法中定义并注册你的工具集。# 假设的插件类结构示例 class MyCustomToolsPlugin: def __init__(self, claw_instance): self.claw claw_instance self._register_tools() def _register_tools(self): # 将函数封装成OpenClaw可识别的工具对象 wiki_tool { name: search_internal_wiki, description: 在公司的内部Wiki知识库中搜索相关文档。, parameters: { type: object, properties: { query: {type: string, description: 搜索关键词}, max_results: {type: integer, description: 返回结果数默认5} }, required: [query] }, function: self.search_internal_wiki # 指向实际的函数 } # 调用OpenClaw的API注册工具 self.claw.register_tool(wiki_tool)这个过程的关键在于确保工具的描述name,description,parameters与执行函数function的签名严格匹配。参数名、类型、顺序都必须一致否则在调用时会出现参数解析错误。3. 实战开发一个“智能周报生成”工具光说不练假把式。我们以一个实际场景为例开发一个相对复杂的工具自动生成研发周报。这个工具需要连接多个数据源Git仓库、项目管理工具执行一系列操作并最终生成结构化摘要。核心需求每周五向OpenClaw输入项目名称它能自动获取该项目本周的代码提交记录、创建的Pull Request和关闭的Issue然后整理成一份格式规范的周报草稿。3.1 第一步拆解任务与设计工具参数这个任务可以拆解为几个子操作但为了保持工具的原子性和复用性我们选择设计一个功能相对聚合的工具而不是三四个零散的小工具。工具名称:generate_dev_weekly_report工具描述: “为指定的软件项目生成本周的研发工作周报。它将获取本周的Git提交、Pull Request和Issue活动并汇总成Markdown格式。需要提供项目在代码仓库和项目管理工具中的标识。”输入参数:project_git_path(string, required): 项目Git仓库的URL或本地路径。project_jira_key(string, required): 项目在Jira或其他工具中的关键键值如“PROJ”。since_date(string, optional): 周报起始日期YYYY-MM-DD。默认值为上周一。until_date(string, optional): 周报结束日期YYYY-MM-DD。默认值为上周日。output_format(string, optional): 输出格式可选markdown或html。默认为markdown。3.2 第二步实现核心工具函数这里会涉及与Git和Jira API的交互。我们需要处理错误、数据清洗和格式化。import subprocess import json from datetime import datetime, timedelta import requests from typing import Dict, Any, Optional def generate_dev_weekly_report( project_git_path: str, project_jira_key: str, since_date: Optional[str] None, until_date: Optional[str] None, output_format: str markdown ) - Dict[str, Any]: 生成研发周报的核心函数。 # 1. 处理默认日期 if not until_date: until_date (datetime.now() - timedelta(daysdatetime.now().weekday() 1)).strftime(%Y-%m-%d) if not since_date: since_date (datetime.strptime(until_date, %Y-%m-%d) - timedelta(days6)).strftime(%Y-%m-%d) report_data { project: project_jira_key, period: f{since_date} 至 {until_date}, commits: [], pull_requests: [], issues_closed: [], summary: } # 2. 获取Git提交记录示例通过git log命令 try: git_log_cmd [ git, -C, project_git_path, log, --since, since_date, --until, until_date, --oneline, --no-merges ] result subprocess.run(git_log_cmd, capture_outputTrue, textTrue, checkTrue) commits result.stdout.strip().split(\n) if result.stdout else [] report_data[commits] commits[:20] # 最多显示20条 except subprocess.CalledProcessError as e: return {success: False, error: fGit命令执行失败: {e.stderr}} except Exception as e: return {success: False, error: f获取Git日志异常: {str(e)}} # 3. 获取Jira Issue信息示例通过REST API try: # 假设已配置好JIRA_API_TOKEN和JIRA_SERVER环境变量 auth (your_email, your_api_token) headers {Content-Type: application/json} # 查询本周关闭的Issue jql fproject {project_jira_key} AND status changed to closed during ({since_date}, {until_date}) jira_url fhttps://your-jira-server/rest/api/2/search?jql{jql}maxResults50 response requests.get(jira_url, authauth, headersheaders) response.raise_for_status() issues response.json().get(issues, []) for issue in issues: report_data[issues_closed].append({ key: issue[key], summary: issue[fields][summary] }) except requests.exceptions.RequestException as e: # 这里不直接失败因为可能只有部分数据源不可用 report_data[issues_closed] [{error: f连接Jira失败: {str(e)}}] # 4. 生成汇总摘要这里可以做得更智能比如用LLM概括 total_commits len(report_data[commits]) total_issues len(report_data[issues_closed]) report_data[summary] f本周项目{project_jira_key}共有{total_commits}次提交关闭了{total_issues}个Issue。 # 5. 根据格式要求渲染最终输出 if output_format markdown: final_output _render_markdown(report_data) else: final_output _render_html(report_data) return { success: True, data: report_data, report: final_output } def _render_markdown(data: Dict) - str: 将数据渲染为Markdown格式 md f# {data[project]} 项目研发周报\n\n md f**报告周期:** {data[period]}\n\n md f## 概要\n{data[summary]}\n\n md ## 本周提交\n for commit in data[commits]: md f- {commit}\n md \n## 本周关闭的Issue\n for issue in data[issues_closed]: if isinstance(issue, dict) and key in issue: md f- {issue[key]}: {issue[summary]}\n else: md f- 数据获取异常\n return md3.3 第三步错误处理与边界考虑在工具开发中健壮性比功能性更重要。上面的代码已经包含了一些基本的try-except但实际还需要考虑更多认证与授权Git可能需要SSH密钥Jira需要API Token。这些敏感信息绝不能硬编码在代码里。最佳实践是通过OpenClaw的插件配置系统传入或者读取环境变量。网络与超时对外部API的调用必须设置超时并做好网络异常的降级处理。例如Jira连接失败时可以选择只返回Git数据并在报告里注明“Jira数据暂不可用”而不是让整个工具崩溃。数据量过大一个活跃的项目一周可能有上百次提交。工具应该设计分页或限制返回条数避免响应数据过大影响AI处理或导致内存问题。日期逻辑我们的默认日期计算上周一到上周日在跨年、跨月时可能需要更精细的处理。可以考虑使用dateutil等库来更鲁棒地处理日期运算。注意工具函数中执行命令行操作如git存在安全风险。如果OpenClaw运行在不受控的服务器环境且工具参数来自不可信的用户输入就必须对输入进行严格的校验和清洗防止命令注入攻击。在生产环境中更推荐使用纯Python的Git库如gitpython来替代直接调用命令行。4. 调试、测试与性能优化让工具稳定可靠工具开发完成后直接丢给AI用很可能出问题。我们需要一套本地化的调试和测试流程。4.1 单元测试与模拟为工具函数编写单元测试是保证质量的基础。使用unittest或pytest框架并利用unittest.mock来模拟外部依赖如Git命令、Jira API调用。# test_weekly_report.py import unittest from unittest.mock import patch, MagicMock from your_plugin import generate_dev_weekly_report class TestWeeklyReportTool(unittest.TestCase): patch(subprocess.run) patch(requests.get) def test_tool_success(self, mock_requests_get, mock_subprocess_run): # 模拟Git命令成功返回 mock_git_result MagicMock() mock_git_result.stdout abc123 Fix login bug\ndef456 Update README mock_git_result.returncode 0 mock_subprocess_run.return_value mock_git_result # 模拟Jira API成功返回 mock_response MagicMock() mock_response.json.return_value { issues: [ {key: PROJ-101, fields: {summary: Bug: Button not clickable}}, {key: PROJ-102, fields: {summary: Feature: Add export function}} ] } mock_response.raise_for_status MagicMock() mock_requests_get.return_value mock_response # 调用工具函数 result generate_dev_weekly_report( project_git_path/fake/repo, project_jira_keyPROJ, since_date2024-01-01, until_date2024-01-07 ) # 断言 self.assertTrue(result[success]) self.assertIn(report, result) self.assertIn(PROJ-101, result[report]) self.assertIn(Fix login bug, result[report]) def test_tool_git_failure(self): # 测试Git命令失败的情况 # ... 模拟 subprocess.CalledProcessError ... pass4.2 在OpenClaw环境中集成测试单元测试通过后需要在真实的OpenClaw插件环境中进行集成测试。本地启动一个测试用的OpenClaw实例。很多开源项目都提供了docker-compose文件可以快速在本地拉起服务。将你的插件代码放到OpenClaw的插件目录具体路径需参考OpenClaw文档通常是plugins/或custom_tools/下。修改配置文件启用你的插件。通过OpenClaw的Web界面或API进行对话测试。直接告诉AI“请使用generate_dev_weekly_report工具为项目X生成上周的周报。” 观察AI是否能够正确理解工具描述、索要参数并成功执行。在这个过程中你可能会发现描述不够准确、参数类型不匹配、返回结果AI无法解析等问题。需要反复调整工具的描述和实现。4.3 性能优化与异步化如果你的工具需要执行耗时的操作如处理大文件、调用慢速API会阻塞AI的响应线程导致用户体验很差。这时需要考虑异步化。异步函数将工具函数定义为async def并在内部使用aiohttp进行网络请求使用asyncio.create_subprocess_exec执行命令行。任务队列对于耗时极长的任务如训练模型工具函数不应同步执行而是应该向一个任务队列如Celery、RQ提交任务并立即返回一个任务ID。然后可以再提供另一个工具如get_task_status来查询任务结果。这需要更复杂的架构设计。# 异步工具函数示例 import aiohttp import asyncio async def async_fetch_webpage(url: str) - dict: 异步获取网页内容 async with aiohttp.ClientSession() as session: try: async with session.get(url, timeout10) as response: text await response.text() return {success: True, content: text[:1000]} # 只取前1000字符 except asyncio.TimeoutError: return {success: False, error: 请求超时} except Exception as e: return {success: False, error: str(e)}在注册异步工具时需要确保OpenClaw的框架支持异步工具的执行和调度。5. 进阶构建工具链与处理复杂依赖单个工具的能力是有限的。真正的威力在于让多个工具协同工作形成“工具链”。AI可以自主规划调用这些工具的先后顺序完成复杂任务。5.1 设计可组合的工具我们的周报生成工具其实已经是一个“组合工具”的雏形。但我们可以把它拆得更细提高复用性get_git_commits(repo_path, since, until): 专用于获取Git提交。get_jira_issues(project_key, status_changed_to, during_period): 专用于查询Jira Issue。format_report(data, format): 专用于格式化数据为报告。这样AI不仅可以用来生成周报当用户问“我们项目上周改了哪些文件”时它可以单独调用get_git_commits问“PROJ-101这个任务关了吗”它可以调用get_jira_issues。工具的粒度更细AI的灵活性更高。5.2 工具间的依赖与状态管理有些工具可能需要共享状态。例如工具A生成了一个临时文件工具B需要读取它。或者工具A登录了一个系统拿到了认证Cookie工具B需要复用这个Cookie。OpenClaw的Agent通常会在一个会话Session中维护一定的上下文状态。你可以通过以下方式处理依赖通过参数传递这是最清晰的方式。工具A的输出结果中包含工具B所需的信息如文件路径、TokenAI在调用工具B时需要将这个信息作为参数传入。这要求AI有良好的上下文理解能力。利用会话状态一些框架允许工具在会话中设置和获取全局变量。但这需要谨慎设计避免状态混乱和内存泄漏。通常更推荐将中间结果以结构化的方式返回由AI来决定如何传递给下一个工具。5.3 让AI学会“思考”在工具描述中加入使用范例这是提升工具使用准确率的一个小技巧。在工具的description字段末尾可以加入一两个“示例”。这相当于给AI提供了少样本Few-Shot提示。tool_description 在公司的内部Wiki知识库中搜索相关文档。 例如 - 当用户问‘我们的项目部署流程是什么’时可以使用此工具搜索‘部署流程’。 - 当用户需要了解‘数据库连接池配置’时可以使用此工具。 参数说明 - query: 搜索关键词尽量具体如‘K8s生产环境部署手册’。 - max_results: 返回结果数默认为5。 虽然OpenClaw的底层大模型不一定显式地解析这些示例但更详细的描述无疑能帮助它更好地理解工具的意图和适用场景。6. 避坑指南从开发到上线的常见问题结合我自己和社区里遇到的一些坑这里总结几个高频问题问题一AI总是不调用我的工具或者说“我没有这个功能”。排查思路检查注册是否成功首先确认你的插件被正确加载工具注册函数被调用。查看OpenClaw启动日志有无错误。检查工具描述这是最常见的原因。description是否足够清晰是否说明了何时使用parameters的description是否让AI明白该填什么试着用你的描述去问一个陌生人看他能否猜出工具的用途和参数。检查工具名称名称是否过于通用如handle_data或与其他工具冲突尝试使用更具体、动词开头的名称。问题二AI调用了工具但参数总是传错比如把字符串传给了数字参数。排查思路严格校验JSON Schema确保parameters中定义的type与你函数参数的类型注解完全一致。string、integer、boolean必须对应Python的str、int、bool。函数签名与文档工具函数的参数名必须和parameters里定义的属性名一致。使用类型注解Type Hints有助于框架进行类型转换。提供枚举值如果参数只有几个固定选项如output_format: [“markdown“, “html”]一定要在Schema中用enum字段明确列出这能极大提高AI传参的准确性。问题三工具执行成功但AI无法理解返回的结果回答变得混乱。排查思路结构化返回确保工具返回的是一个字典或列表而不是一个复杂的自定义对象或冗长的纯文本。AI更擅长处理结构化的键值对。简化与摘要如果操作结果数据量很大如查询数据库返回100行不要在返回结果里包含全部数据。应该在工具内部先做一次聚合、摘要或只取前N条关键数据。清晰的成功/失败标志返回的字典里最好有一个success: true/false字段和一个可选的error或message字段。这能帮助AI快速判断工具执行状态。问题四工具涉及敏感操作如删除文件、调用生产环境API如何控制权限解决方案环境隔离为开发、测试、生产环境部署不同的OpenClaw实例和工具集。生产环境的工具插件必须经过严格评审。参数校验与沙箱在工具函数内部对输入参数进行白名单校验。对于执行命令或代码的工具考虑在沙箱环境如Docker容器中运行。权限标签可以在工具描述中增加一个risk_level或requires_auth的元数据。在OpenClaw的服务端可以根据用户角色或对话上下文来决定是否展示或允许调用该工具。这需要框架层面的支持。开发一个稳定、好用的OpenClaw Agent工具三分在编码七分在设计和调试。最重要的始终是站在AI的角度思考它如何理解你的描述它如何组合这些工具来解决问题把这个过程想通了你开发出的就不再是一个简单的脚本插件而是一个真正能扩展AI能力边界的“智能模块”。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进