技能使用分析工具:从日志复盘到数据驱动的效率优化
1. 项目概述一个专为技能使用复盘而生的分析工具如果你和我一样是OpenClaw的深度用户并且已经用上了skill-logger来记录每一次技能调用的反馈那么你大概率会遇到一个甜蜜的烦恼日志文件越积越多数据越来越杂但真正有价值的使用洞察却依然藏在那一行行JSONL里需要手动去翻找、统计。Yidoll/skill-analyzer这个项目就是为了解决这个痛点而生的。它不是一个泛泛的数据分析工具而是一个精准的手术刀专门用来解剖skill-logger生成的Obsidian日志帮你把“最近哪些技能用得顺手”、“哪些技能评分不高需要优化”、“哪些老技能已经吃灰了”这些问题用一份清晰的数据报告呈现出来。简单来说它就是一个技能使用情况的“体检报告生成器”。你只需要告诉它“分析一下过去30天我的技能使用情况”它就能从你指定的Obsidian库的固定路径里读取日志然后生成包含使用次数、平均满意度、高频技能排行、低分技能清单等一系列维度的分析结果。这对于个人效率复盘、技能集优化甚至是评估某个新上线的技能是否真的解决了问题都提供了非常直观的数据支撑。这个项目是原始skill-analyzer思路的重写版但针对OpenClaw的环境做了大量加固和优化用起来更明确、更健壮也更能避免一些因环境配置模糊导致的“幽灵数据”问题。2. 核心设计思路从“能用”到“好用且可靠”的进化这个重写版的核心设计哲学非常明确消除不确定性强化边界意识。原始的技能分析工具可能为了“用户友好”而做了过多的静默处理和默认回退但这在数据分析场景下是危险的。一个错误的环境变量可能导致脚本读取了完全错误的日志文件生成一份毫无意义的报告而用户却浑然不知。因此这个版本的改进点都围绕着如何让整个过程更可控、更透明。2.1 强制环境依赖与明确报错机制最显著的一个改进是强制要求OBSIDIAN_VAULT环境变量。原始版本可能会在找不到环境变量时尝试回退到某个默认的或猜测的Obsidian库路径。这种“静默回退”在工具类脚本中或许是便利但在数据分析脚本中就是灾难的源头。本版本的设计是如果OBSIDIAN_VAULT没有设置或者设置的路径不存在、不可读那么脚本会直接、明确地报错并退出告诉你“环境未配置”或“路径无效”而不是尝试去一个错误的地方读取数据。注意这种设计虽然看起来“不友好”但它保证了数据源的绝对正确性。在数据分析领域输入数据的质量直接决定了输出结论的可信度。一个明确的错误远比一份基于错误数据生成的、看似完美的错误报告要有价值得多。这迫使我们在使用前必须正确地设置环境养成好习惯。2.2 基于日期的精细化时间窗口过滤另一个关键改进是用日期粒度进行时间窗口过滤。技能使用日志通常是按时间顺序追加的我们分析时往往关心的是“最近一周”、“上个月”这样的时间段。原始版本可能只做了简单的行数截取或粗糙的时间戳比对。重写版则强调基于日志条目中的日期字段假设你的skill-logger记录了timestamp进行精确的日期范围过滤。这意味着当你要求分析“最近7天”时脚本会计算出从今天往前推7天的日期边界然后只处理那些时间戳落在这个边界内的日志条目。这种基于自然日期的过滤比单纯“读取最后100行”要合理得多因为它不受你当天使用频率的影响能真实反映一个周期内的使用情况。2.3 对数据脏读的容忍与记录日志文件在长期写入过程中难免会因为程序异常中断、版本变更、手动编辑等原因出现个别格式错误、字段缺失或结构不符的行即“坏数据”。一个健壮的分析脚本不应该因为一行坏数据就让整个分析任务崩溃。这个重写版加强了对坏行和旧格式的耐受性。其策略通常是在逐行读取JSONL文件时使用try-except块包裹每一行的JSON解析操作。对于能成功解析且符合预期结构的行纳入统计对于解析失败或结构校验不通过的行则将其归类到“被跳过的坏数据行”中并在最终报告里明确列出跳过了多少行。这样既保证了核心分析流程的顺利进行又让用户知晓数据完整性的情况方便后续去检查并修复那些坏行。2.4 结构化输出与真正的“历史对比”功能输出方面除了友好的人类可读文本如命令行打印脚本还支持结构化的JSON输出。这对于自动化流程非常重要。你可以将JSON输出重定向到一个文件然后用其他脚本或工具如Jupyter Notebook、Grafana进行二次处理或可视化集成到更庞大的个人数据看板中。此外重写版真正实现了“历史出现过但窗口内未使用的技能”统计。这个功能很有价值。它的实现逻辑通常是首先扫描整个日志文件或足够长的历史记录建立一个“全量技能名称集合”。然后在指定的时间窗口内再次统计实际被使用过的技能集合。最后对两个集合做差集运算全量集合 - 窗口内使用集合 窗口内未使用集合。这样你就能清晰地看到有哪些技能你曾经掌握并使用过但在最近的分析周期内一次都没调用过它们可能就是你需要重新捡起来或者考虑归档的“冷技能”。3. 实战部署与核心脚本解析理论说得再多不如动手配置一遍来得实在。下面我将带你一步步完成skill-analyzer的部署并深入解析其核心脚本analyze_skill_usage.py的关键部分让你不仅会用还能懂其原理。3.1 环境准备与前置条件首先确保你已经具备以下条件一个正在运行的OpenClaw环境这是技能运行的基础。已安装并配置skill-logger技能这是数据来源的保证。你需要确认skill-logger正在正常工作并将日志写入到正确的路径。明确的Obsidian知识库路径这是本技能强制要求的。找到你的Obsidian库在本地的绝对路径。接下来设置强制要求的环境变量。打开你的终端比如~/.bashrc,~/.zshrc或OpenClaw的启动脚本添加如下行export OBSIDIAN_VAULT/Users/你的用户名/Documents/Obsidian Vaults/MyKnowledgeBase请务必将路径替换为你自己的真实路径。保存后执行source ~/.zshrc或对应的配置文件使其生效或者直接在新终端中运行上述export命令。你可以通过echo $OBSIDIAN_VAULT来验证是否设置成功。3.2 技能安装与校验获取skill-analyzer技能包。通常你可以从项目的Release页面下载打包好的skill-analyzer.skill文件。为了安全起见务必进行完整性校验。# 假设你已经将 skill-analyzer.skill 和 SHA256SUMS.txt 下载到当前目录 shasum -a 256 -c SHA256SUMS.txt如果终端显示skill-analyzer.skill: OK说明文件完好无损。然后你可以通过OpenClaw的管理界面或命令行工具将这个.skill文件安装到你的技能目录中。安装成功后在OpenClaw的技能列表里应该能看到skill-analyzer。3.3 核心脚本analyze_skill_usage.py深度解析让我们打开scripts/analyze_skill_usage.py看看它是如何工作的。我会摘取关键代码段并加以解释。1. 环境检查与路径构建脚本开头一定会检查OBSIDIAN_VAULT环境变量。这是安全性的第一道关卡。import os import json from datetime import datetime, timedelta from collections import defaultdict, Counter from pathlib import Path def main(): vault_path os.environ.get(OBSIDIAN_VAULT) if not vault_path: print(错误未设置 OBSIDIAN_VAULT 环境变量。) print(请先执行export OBSIDIAN_VAULT/your/obsidian/vault/path) return log_file_path Path(vault_path) / 06 计划 / skill_usage_log.jsonl if not log_file_path.is_file(): print(f错误找不到日志文件 {log_file_path}) print(请确保 skill-logger 已正确运行并生成日志。) return这段代码逻辑清晰没有环境变量就报错退出有环境变量就构建出日志文件的完整路径并检查文件是否存在。这种“早失败”原则避免了后续更隐蔽的错误。2. 时间窗口参数解析脚本需要接收一个时间窗口参数比如--days 7。它会据此计算起始日期。import argparse parser argparse.ArgumentParser(description分析skill使用日志) parser.add_argument(--days, typeint, default30, help分析最近多少天的数据默认30天) args parser.parse_args() window_end datetime.now() window_start window_end - timedelta(daysargs.days) print(f分析时间窗口{window_start.date()} 至 {window_end.date()} ({args.days}天))3. 日志读取与容错处理这是体现“健壮性”的关键部分。脚本会逐行读取JSONL文件并优雅地处理坏数据。stats { total_entries: 0, valid_entries: 0, skipped_lines: 0, skills_counter: Counter(), # 技能使用次数统计 satisfaction_sum: 0.0, # 满意度总分用于计算平均分 satisfaction_count: 0, # 有满意度评分的条目数 all_skills_seen: set(), # 历史上出现过的所有技能 window_skills_used: set(), # 时间窗口内使用过的技能 low_rated_skills: [] # 低评分技能列表例如评分2 } malformed_lines [] with open(log_file_path, r, encodingutf-8) as f: for line_num, line in enumerate(f, 1): stats[total_entries] 1 line line.strip() if not line: continue # 跳过空行 try: entry json.loads(line) # 基础结构校验 if not isinstance(entry, dict) or skill_name not in entry: raise ValueError(日志行缺少skill_name字段或不是有效JSON对象) skill_name entry[skill_name] stats[all_skills_seen].add(skill_name) # 检查时间戳是否在窗口内 timestamp_str entry.get(timestamp) if timestamp_str: try: entry_time datetime.fromisoformat(timestamp_str.replace(Z, 00:00)) except ValueError: # 时间戳格式不符跳过时间过滤但依然处理技能名 entry_time None else: entry_time None in_window (entry_time and window_start entry_time window_end) if entry_time else False if in_window: stats[valid_entries] 1 stats[window_skills_used].add(skill_name) stats[skills_counter][skill_name] 1 # 处理满意度评分 satisfaction entry.get(satisfaction) if satisfaction is not None and isinstance(satisfaction, (int, float)): stats[satisfaction_sum] satisfaction stats[satisfaction_count] 1 if satisfaction 2: # 假设1-5分制2分为低分 stats[low_rated_skills].append({ skill: skill_name, rating: satisfaction, time: timestamp_str }) except (json.JSONDecodeError, ValueError) as e: stats[skipped_lines] 1 malformed_lines.append({ line_number: line_num, line_content: line[:100] ... if len(line) 100 else line, # 只记录前100字符 error: str(e) }) continue # 跳过这行继续处理下一行这段代码是脚本的核心逐行处理使用enumerate记录行号便于定位错误。异常捕获try-except块捕获JSON解析错误和自定义的结构校验错误。数据校验检查skill_name是否存在尝试解析timestamp。时间过滤只有时间戳存在且落在窗口内的条目才被计入窗口期统计。历史记录无论是否在窗口内只要技能名有效就加入all_skills_seen集合。低分记录收集低满意度条目便于后续重点回顾。坏行记录将坏行的行号、片段和错误原因保存下来最后汇报给用户。4. 数据分析与报告生成所有数据收集完毕后开始计算各项指标。# 计算基本指标 unique_skills_in_window len(stats[window_skills_used]) total_uses_in_window sum(stats[skills_counter].values()) avg_satisfaction (stats[satisfaction_sum] / stats[satisfaction_count]) if stats[satisfaction_count] 0 else None # 高频技能排行取前10 top_skills stats[skills_counter].most_common(10) # 历史出现过但窗口内未使用的技能 skills_unused_in_window stats[all_skills_seen] - stats[window_skills_used] # 生成报告 report { 分析周期: f{args.days}天 ({window_start.date()} 至 {window_end.date()}), 日志文件: str(log_file_path), 数据概览: { 日志总行数: stats[total_entries], 有效分析条目数窗口内: stats[valid_entries], 跳过坏数据行数: stats[skipped_lines], 窗口内使用技能数去重: unique_skills_in_window, 窗口内总使用次数: total_uses_in_window, }, 满意度分析: { 平均满意度: round(avg_satisfaction, 2) if avg_satisfaction else 无评分数据, 参与评分的条目数: stats[satisfaction_count] }, 技能使用排行: [{技能: skill, 次数: count} for skill, count in top_skills], 低满意度技能2分: stats[low_rated_skills], 近期未使用技能: sorted(list(skills_unused_in_window)), 数据质量备注: { 跳过的坏行示例: malformed_lines[:3] if malformed_lines else 无 # 最多展示3条 } } # 输出报告 print(\n *50) print(Skill 使用分析报告) print(*50) # ... 这里将report字典以友好格式打印出来 ... # 同时也可以提供一个选项将report以JSON格式输出到文件这个结构化的report字典包含了所有分析维度。你可以选择将其美观地打印在终端也可以通过json.dump(report, open(report.json, w), ensure_asciiFalse, indent2)输出到文件供其他程序使用。4. 使用场景、工作流与进阶技巧理解了原理和部署我们来聊聊怎么把它用出花来。skill-analyzer不是一个每天都要运行的监控工具而是一个定期的复盘工具。4.1 明确的使用场景与触发条件正如项目文档强调的这个技能有非常明确的使用边界。它只在用户明确要求分析技能使用情况时激活。以下是一些正确的“唤醒词”示例“分析一下我最近一周的技能使用情况。”“给我出一份过去一个月的技能使用报告。”“看看我这三个月哪个技能用得最多哪个评分最低。”“统计一下2024年Q2所有技能的使用频率。”而以下情况不应该触发skill-analyzer“总结一下我昨天的笔记。”这是泛化总结应用其他笔记总结技能“分析我的数据。”过于模糊没有指定是“技能使用数据”“最近效率怎么样”问题不明确可能指向时间管理或任务完成度分析在OpenClaw的SKILL.md定义中会通过清晰的description和examples来约束技能的触发意图确保它不会被误调用。4.2 与skill-logger的黄金工作流skill-analyzer的价值建立在skill-logger稳定记录的基础上。两者构成了一个完整的“记录-复盘”闭环。无缝记录确保skill-logger作为你的默认技能反馈记录器。每当你完成一个技能调用尤其是那些解决具体问题的技能如git-commit-helper,code-reviewer,meeting-summarizerOpenClaw会提示你给予满意度评分例如1-5分。skill-logger会默默地将技能名、时间戳、满意度、可能还有上下文简述写入到$OBSIDIAN_VAULT/06 计划/skill_usage_log.jsonl文件中。这个过程应该是无感的、自动的。定期复盘每周或每月的复盘时间你主动调用skill-analyzer。指定一个时间窗口如--days 7让它生成报告。报告会告诉你高频技能哪些是你 workflow 中的核心工具它们的价值是否被充分挖掘低分技能哪些技能用起来不满意是技能本身设计有问题还是你的使用场景不对这为你优化技能或调整使用习惯提供了直接依据。闲置技能哪些技能很久没用了是需求消失了还是你忘记了它的存在这能提醒你清理技能列表或者重新学习某个被遗忘的利器。行动与优化基于报告采取行动。对于高频高满意度的技能考虑是否可以将其更深地集成到你的工作流中。对于低分技能去查看具体的低分记录报告里可能有时间戳回忆当时的使用场景思考是技能需要改进还是自己需要改变使用方法。你甚至可以基于这些反馈去给技能开发者提Issue。对于长期闲置的技能问自己两个问题我未来还需要它吗如果不需要可以考虑禁用或卸载如果需要可以安排时间重新熟悉一下。4.3 进阶技巧与自定义分析基础报告已经很有用但你可以通过一些技巧挖掘更深层的价值。1. 生成结构化JSON进行二次分析修改脚本或在调用时增加参数使其将report字典直接输出为JSON文件。然后你可以用Python的Pandas、Jupyter Notebook甚至是Obsidian的Dataview插件对这些数据进行可视化。# 假设脚本支持 --output-json 参数 python analyze_skill_usage.py --days 30 --output-json monthly_report.json接着你可以写一个简单的Python脚本用Matplotlib画图import json import matplotlib.pyplot as plt with open(monthly_report.json, r) as f: data json.load(f) skills [item[技能] for item in data[技能使用排行]] counts [item[次数] for item in data[技能使用排行]] plt.figure(figsize(10,6)) plt.barh(skills, counts) plt.xlabel(使用次数) plt.title(过去30天技能使用频率TOP10) plt.tight_layout() plt.savefig(skill_usage_top10.png)2. 追踪技能满意度趋势skill-analyzer目前提供的是时间窗口内的平均分。你可以修改脚本让它计算同一个技能在不同时间段比如每周的平均分从而观察其满意度是上升、下降还是稳定。这能帮你评估技能迭代的效果或者发现某个技能在特定类型任务上表现不佳。3. 与任务/项目关联分析高级如果你的skill-logger在记录时还能通过某种方式比如从OpenClaw的上下文中捕获当前正在处理的项目或任务标签并一同记录到日志中。那么skill-analyzer就可以增强为按项目分析技能使用情况回答诸如“在A项目中最依赖哪些技能”、“哪个技能在跨项目中通用性最高”这类问题。这需要对skill-logger和skill-analyzer都进行定制化开发。5. 常见问题、排查与避坑指南即使设计得再健壮在实际使用中也可能遇到问题。下面是我在部署和使用过程中遇到的一些典型情况及解决方法。5.1 环境与路径问题问题运行技能时报错“未设置 OBSIDIAN_VAULT 环境变量”。排查在运行OpenClaw或调用技能的终端里执行echo $OBSIDIAN_VAULT。如果输出为空说明环境变量未生效。解决确认你是在同一个终端会话中设置了环境变量并启动了OpenClaw。如果是在A终端export在B终端启动则B终端无效。将export OBSIDIAN_VAULT...这行命令永久添加到你的shell配置文件~/.bashrc,~/.zshrc等中然后重启终端或执行source ~/.zshrc。如果你通过图形界面启动OpenClaw可能需要修改其启动脚本或桌面快捷方式确保环境变量被传递进去。问题报错“找不到日志文件”。排查检查$OBSIDIAN_VAULT/06 计划/skill_usage_log.jsonl这个完整路径是否存在。注意06 计划是中文文件夹名确保路径中的空格和中文正确无误。确认skill-logger技能是否成功安装并运行过。如果从未使用过skill-logger这个文件可能不存在。解决手动创建缺失的目录和文件但空文件没有数据。mkdir -p $OBSIDIAN_VAULT/06 计划 touch $OBSIDIAN_VAULT/06 计划/skill_usage_log.jsonl。先主动使用几次其他技能并确保skill-logger被触发记录生成日志条目。5.2 数据分析结果异常问题报告显示“有效分析条目数”为0但我知道最近用过技能。排查时间窗口问题检查你指定的--days参数是否过小或者你是否在分析一个未来的日期范围时间戳格式问题这是最常见的原因。查看日志文件的前几行确认timestamp字段的格式。脚本期望的是ISO 8601格式如2023-10-27T10:30:00Z。如果skill-logger记录的格式不同脚本的datetime.fromisoformat解析会失败导致entry_time为None从而不被计入窗口。时区问题本地时间与UTC时间的差异可能导致日期判断偏差。解决先用一个较大的--days参数如1000运行看看是否能统计到数据。如果能说明是时间窗口设得太近。修改脚本中的时间解析部分使其兼容你的日志时间格式。例如如果日志时间是2023/10/27 10:30:00你需要使用datetime.strptime(timestamp_str, %Y/%m/%d %H:%M:%S)来替换fromisoformat。在时间比较时可以考虑将时间戳和窗口边界都转换为UTC时间或本地时间后再比较确保一致性。问题“近期未使用技能”列表包含了大量我明明最近用过的技能。排查这通常是“技能名不一致”导致的。skill-logger记录的技能名和skill-analyzer用于统计的技能名必须完全一致包括大小写、空格、版本号等。解决检查日志文件找到你认为最近使用过的技能条目看其skill_name字段具体是什么。对比“近期未使用技能”列表里的名字看是否存在细微差别。例如“git-helper”和“git_helper”会被视为两个不同的技能。确保技能的定义是稳定的。如果技能更新后改名了那么旧名字的技能就会进入“未使用”列表。这种情况下你可以选择清洗历史日志有风险或者接受这种“自然更替”的统计。5.3 性能与日志管理问题日志文件非常大超过100MB脚本运行缓慢。排查JSONL文件是追加写入的长期不清理会变得巨大。逐行读取和解析整个文件确实会慢。解决日志轮转这是最推荐的做法。可以写一个简单的定时任务cron job每周或每月将当前的skill_usage_log.jsonl重命名为带日期的备份文件如skill_usage_log_202410.jsonl然后新建一个空的当前日志文件。skill-analyzer脚本可以修改为支持读取多个历史日志文件进行分析。分析时过滤在脚本中一旦读取的行时间戳远早于分析窗口的起始时间比如你分析最近7天但日志有3年数据可以提前跳出循环因为更早的数据肯定不在窗口内。但这需要日志是按时间顺序写入的通常都是。使用更高效的数据结构对于超大型文件可以考虑使用pandas的read_json指定linesTrue来读取其底层是C实现的速度更快。但这会引入新的依赖。问题报告中的“平均满意度”是NaN或无数据。排查检查日志中satisfaction字段是否存在以及是否为数字。早期版本的skill-logger可能没有记录该字段或者用户跳过了评分。解决确保你使用的skill-logger版本支持满意度评分。在调用技能后养成给予评分的习惯。如果没有评分那么满意度分析就失去了意义。在脚本中对satisfaction_count为0的情况做友好提示而不是显示一个错误或NaN。5.4 一个真实的避坑案例时区陷阱我曾经遇到一个诡异的问题在周五晚上本地时间周六凌晨分析“最近1天”的数据结果一条记录都没有。经过排查发现日志中的timestamp是UTC时间2024-10-25T16:00:00Z而脚本中datetime.now()获取的是本地时间北京时间2024-10-26 00:00:00。当我计算window_start datetime.now() - timedelta(days1)时得到的是北京时间2024-10-25 00:00:00。而UTC时间2024-10-25T16:00:00Z转换成北京时间是2024-10-26 00:00:00正好等于window_end但可能因为微小的精度问题不被认为在区间内或者刚好落在边界上被错误过滤。解决方案在脚本中统一使用UTC时间进行处理。from datetime import datetime, timedelta, timezone # 获取当前UTC时间 window_end datetime.now(timezone.utc) window_start window_end - timedelta(daysargs.days) # 解析日志时间戳时如果字符串以Z结尾直接解析为UTC时间 entry_time datetime.fromisoformat(timestamp_str.replace(Z, 00:00)).replace(tzinfotimezone.utc)这样所有时间比较都在同一时区UTC下进行彻底避免了时区转换带来的边界错误。这个坑提醒我们在处理时间数据时永远要在内部使用UTC仅在展示时转换为本地时间。