用Python+本地大模型打造自动化周报工具:200行代码实现无感工时记录
1. 项目概述一个程序员的“反内卷”实践每周五下午你是不是也和我一样对着空白的周报文档发呆脑子里像过电影一样回顾一周的工作却怎么也拼凑不出那些零碎的、看似“不值一提”的工时片段。写周报这件本该是总结和复盘的好事却常常因为耗时耗力、流于形式而变成一种负担。更别提那些临时插入的会议、突如其来的线上支持、帮同事排查的一个小问题它们消耗了你的时间却在周报里“查无此人”。作为一名开发我本能地想到用技术来解决这个“痛点”。市面上当然有成熟的工时管理软件但它们要么太重需要复杂的项目、任务配置要么太贵对个人或小团队不友好要么就是数据隐私让人担忧。我的需求其实很简单自动、无感、本地化地记录我在电脑上花了时间做什么并在周末自动生成一份结构清晰的周报草稿。于是这个“AI工时助手”的想法诞生了。它的核心逻辑是通过一个常驻后台的轻量级程序静默记录我的活动窗口比如正在使用的IDE、浏览器标签、会议软件然后利用本地运行的大语言模型LLM对这些原始、琐碎的日志进行智能分析和总结最终输出一份带有时间分配、工作内容归纳的周报。整个项目我用大约200行Python代码实现了核心闭环。它不依赖任何云端API所有数据处理都在本地完成既保护了隐私又实现了真正的“开机即用关屏即走”。2. 核心设计思路与技术选型这个项目的目标很明确做一个极简、本地优先的自动化周报工具。因此在设计之初我就定下了几个核心原则无侵入性不能影响我正常工作的流畅度最好让我感觉不到它的存在。本地化与隐私所有活动记录和数据处理必须在本地完成不上传任何隐私信息。低成本与易部署依赖尽可能少部署简单最好一台电脑、一条命令就能跑起来。结果导向生成的周报草稿要有用不能是流水账要能体现工作类别和重点。基于这些原则我拆解了技术栈2.1 活动记录层系统原生接口是首选记录“正在做什么”本质就是获取当前激活的窗口标题。这里没有选择复杂的图像识别或网络流量分析而是直接调用操作系统提供的API。Windows系统使用pywin32库调用user32DLL通过GetForegroundWindow和GetWindowTextW函数可以稳定、高效地获取当前焦点窗口的标题。这是最轻量、最可靠的方式。macOS/Linux系统也有对应的方案比如macOS可以使用AppKitLinux可以使用ewmh通过python-xlib。为了简化我的初版只实现了Windows因为这是我主要的工作环境。跨平台是很好的扩展方向但MVP最小可行产品阶段先解决主要矛盾。注意直接获取窗口标题会包含大量“噪音”比如浏览器标签页的完整标题可能很长“如何解决xxx问题 - 知乎 - Google Chrome”。我们需要的是核心信息“如何解决xxx问题 - 知乎”后续的清洗和聚合逻辑很重要。2.2 日志存储层轻量级数据库足矣记录下来的数据需要持久化。考虑到数据量不大每秒记录一次一天也就几万条记录但查询需要按时间范围灵活我选择了SQLite。它无需安装服务器单个文件管理Python原生支持完美符合“本地化”和“轻量”的要求。表结构设计也非常简单CREATE TABLE activity_logs ( id INTEGER PRIMARY KEY, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, window_title TEXT, process_name TEXT );process_name字段通过psutil库获取可以帮助我们更好地分类活动比如区分是用chrome.exe在查资料还是用devenv.exe(Visual Studio) 在写代码。2.3 智能分析层本地大模型是灵魂这是项目的核心。我们需要一个“大脑”来理解这些零散的日志条目并归纳成人类可读的周报。使用云端AI API如GPT-4固然强大但不符合“本地化”和“低成本”原则。因此我选择了Ollama。为什么是OllamaOllama 是一个强大的工具它让你能在本地轻松运行、管理和与各种开源大模型如 Llama 3、Mistral、Gemma 等交互。它封装了模型加载、推理优化等复杂过程提供了简单的命令行和API接口。这意味着我可以用几行代码就像调用一个本地服务一样使用大模型的能力。模型选择对于文本总结和归纳任务不需要追求千亿参数的最新模型。我选择了Llama 3 8B或Mistral 7B这类较小的模型。它们在总结、改写任务上表现足够好并且在消费级显卡甚至只有CPU上也能跑出可接受的速度。Ollama的模型仓库提供了丰富的选择一键拉取即可。提示词工程如何让大模型理解我们的需求是关键。我设计的提示词Prompt模板大致如下你是一个高效的工作助理。请根据以下我一周的电脑活动日志记录生成一份简洁的周报草稿。要求 1. 将日志按“软件开发”、“会议沟通”、“调研学习”、“事务处理”等类别进行归纳。 2. 为每个类别总结主要工作内容突出关键任务和进展。 3. 时间统计上可以估算各类别的总耗时占比。 4. 语言风格专业、简洁、突出重点。 5. 输出格式先一个简短概述然后分点列出各个类别的工作总结。 以下是一周的日志样本[此处插入从SQLite中查询并格式化后的日志文本]通过精心设计的提示词可以引导模型忽略无关细节如具体的错误信息、无关的网页内容聚焦于工作性质的识别和内容提炼。2.4 调度与输出层自动化闭环最后需要让整个流程自动运行。我使用了Python内置的schedule库或apscheduler库。记录任务以一个固定的时间间隔如10秒或30秒运行将当前窗口信息写入SQLite。这个间隔需要权衡太短会产生大量冗余数据太长可能丢失快速切换的上下文。30秒是一个不错的平衡点。生成任务在每周五下午6点自动触发周报生成脚本。该脚本会从SQLite中查询过去7天的日志。对日志进行初步清洗和聚合例如合并连续相同的窗口标题过滤掉“闲置”、“锁屏”等无效记录。将处理后的文本送入Ollama服务获取生成的周报文本。将周报文本保存为Markdown文件并复制到系统剪贴板方便我直接粘贴到公司的周报系统中。至此一个完整的、自动化的本地AI工时助手的技术蓝图就清晰了。3. 核心代码实现与关键细节理论清晰后我们来看代码如何落地。以下是核心模块的拆解和关键代码片段我会重点解释那些容易踩坑的细节。3.1 活动记录模块的实现这个模块需要常驻后台稳定且低调地运行。import time import sqlite3 from datetime import datetime import psutil import win32gui import win32process class ActivityTracker: def __init__(self, db_pathactivity_log.db): self.db_path db_path self._init_db() def _init_db(self): 初始化数据库表 conn sqlite3.connect(self.db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS activity_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, window_title TEXT, process_name TEXT ) ) conn.commit() conn.close() def get_active_window_info(self): 获取当前活动窗口信息和进程名 try: hwnd win32gui.GetForegroundWindow() window_title win32gui.GetWindowText(hwnd) # 获取进程ID和名称 _, pid win32process.GetWindowThreadProcessId(hwnd) process psutil.Process(pid) process_name process.name() return window_title, process_name except Exception as e: # 当窗口快速切换或进程权限问题时可能会抛出异常记录并忽略 print(f获取窗口信息失败: {e}) return None, None def log_activity(self, interval_seconds30): 按间隔记录活动 conn sqlite3.connect(self.db_path) cursor conn.cursor() try: while True: title, process self.get_active_window_info() if title and process: # 过滤掉无效记录 # 简单的去重如果和上一条记录完全相同则跳过可选 cursor.execute( INSERT INTO activity_logs (window_title, process_name) VALUES (?, ?) , (title, process)) conn.commit() time.sleep(interval_seconds) except KeyboardInterrupt: print(\n记录器停止。) finally: conn.close()关键细节与避坑指南异常处理GetForegroundWindow和GetWindowText在某些特殊窗口如UAC权限提示框、全屏游戏下可能失败psutil.Process也可能因权限问题无法访问某些系统进程。必须用try-except包裹避免程序因单个异常而崩溃。去重逻辑上述代码每次间隔都会记录。你可以增加一个逻辑只在窗口标题或进程名发生变化时才记录这样可以极大减少数据量。但要注意有些长时间操作如编写文档窗口标题不变但实际工作在进行。我的经验是保留原始记录在后续分析阶段做更智能的聚合。进程名 vs. 窗口标题process_name非常有用。你可以建立一个映射表将chrome.exe映射为“浏览器/调研”将devenv.exe或pycharm64.exe映射为“软件开发”将Teams.exe映射为“会议沟通”。这为后续的AI分析提供了强有力的先验分类依据。3.2 与Ollama交互的模块Ollama安装后默认会在http://localhost:11434提供一个API服务。我们需要与之通信。import requests import json class OllamaClient: def __init__(self, base_urlhttp://localhost:11434, modelllama3:8b): self.base_url base_url self.model model def generate_summary(self, prompt, system_promptNone): 调用Ollama API生成总结 url f{self.base_url}/api/generate payload { model: self.model, prompt: prompt, stream: False, # 我们一次性获取结果不流式输出 options: { temperature: 0.2, # 温度调低让输出更稳定、更确定 top_p: 0.9, } } if system_prompt: payload[system] system_prompt try: response requests.post(url, jsonpayload, timeout120) # 设置较长超时 response.raise_for_status() result response.json() return result.get(response, ).strip() except requests.exceptions.RequestException as e: print(f调用Ollama API失败: {e}) return None def prepare_logs_for_prompt(db_path, days7): 从数据库查询并格式化日志作为Prompt的输入 conn sqlite3.connect(db_path) cursor conn.cursor() # 查询过去N天的日志按时间排序 cursor.execute( SELECT timestamp, window_title, process_name FROM activity_logs WHERE date(timestamp) date(now, ?) ORDER BY timestamp , (f-{days} days,)) rows cursor.fetchall() conn.close() # 格式化将每条日志变成一行清晰的文本 log_lines [] for ts, title, process in rows: # 简单的清洗去除过长的标题提取核心部分 clean_title title[:100] # 截断过长的标题 log_lines.append(f[{ts}] {process}: {clean_title}) return \n.join(log_lines)关键细节与避坑指南Ollama服务状态在调用API前务必确保Ollama服务正在运行。可以写一个检查函数或者让生成任务在失败时给出明确提示。提示词长度与模型上下文一周的日志压缩后可能仍有几千字。需要确保你选择的模型支持这个长度的上下文。Llama 3 8B通常支持8K上下文足够使用。如果日志太长可以考虑先按天做一次初步的聚合摘要再将每日摘要交给模型做周汇总这是一种“分层总结”的策略。参数调优temperature参数控制创造性。对于总结任务设置为较低值如0.1-0.3可以获得更稳定、更事实性的输出。top_p参数也可以用来控制输出的多样性。错误处理网络请求可能失败模型可能未加载。必须做好异常处理并考虑降级方案比如生成一个基于简单规则如按进程名统计时间的备选报告。3.3 周报生成与调度主模块这是粘合所有部分的“胶水”。import schedule import time from datetime import datetime import pyperclip # 用于操作剪贴板 def job_generate_weekly_report(): print(f{datetime.now()}: 开始生成周报...) # 1. 准备日志数据 logs_text prepare_logs_for_prompt(activity_log.db, days7) if not logs_text or len(logs_text.split(\n)) 10: print(日志数据不足跳过本周报生成。) return # 2. 构建最终Prompt system_prompt 你是一个专业、高效的工作总结助理。请根据提供的电脑活动日志生成一份结构清晰、重点突出的工作周报草稿。 user_prompt f请根据我过去一周的电脑活动日志生成一份工作周报草稿。 要求 1. 将工作内容归纳为“核心开发”、“会议与协作”、“技术调研”、“其他事务”等类别。 2. 每个类别下总结具体的任务和进展避免罗列原始日志。 3. 在开头给出一个简要的总体概述。 4. 输出使用Markdown格式。 以下是原始日志格式为 [时间] 进程名: 窗口标题 {logs_text} # 3. 调用AI client OllamaClient(modelmistral:7b) report client.generate_summary(user_prompt, system_prompt) # 4. 保存并输出 if report: filename fweekly_report_{datetime.now().strftime(%Y%m%d)}.md with open(filename, w, encodingutf-8) as f: f.write(report) print(f周报已保存至: {filename}) # 复制到剪贴板方便粘贴 pyperclip.copy(report) print(周报内容已复制到剪贴板。) else: print(周报生成失败。) def main(): # 启动活动记录器在后台线程中运行 tracker ActivityTracker() import threading record_thread threading.Thread(targettracker.log_activity, daemonTrue) record_thread.start() # 设置每周五18:00生成周报 schedule.every().friday.at(18:00).do(job_generate_weekly_report) print(AI工时助手已启动。每周五18:00自动生成周报。) while True: schedule.run_pending() time.sleep(60) # 每分钟检查一次任务 if __name__ __main__: main()关键细节与避坑指南后台运行活动记录是一个无限循环必须放在单独的线程中运行以免阻塞主线程的调度任务。将其设置为守护线程daemonTrue这样当主程序退出时它会自动结束。调度可靠性schedule库简单易用但它在系统休眠唤醒后可能会错过任务。对于生产环境可以考虑使用系统的定时任务如Windows任务计划程序或Linux的cron来调用周报生成脚本这样更可靠。数据过滤在prepare_logs_for_prompt函数中可以加入更强大的过滤逻辑。例如过滤掉包含“YouTube”、“Steam”、“个人邮箱”等明显非工作相关的窗口标题或者根据进程名直接排除一些娱乐软件。这能显著提升AI生成周报的专业性和准确性。剪贴板集成使用pyperclip将结果复制到剪贴板是一个提升体验的小技巧让你能一键粘贴到企业微信、钉钉或Confluence等平台。4. 部署、优化与个性化调整代码跑起来只是第一步要让工具真正好用还需要一些部署技巧和优化点。4.1 部署为后台服务我们不想总是开着一个命令行窗口。在Windows上可以将其包装为系统服务或使用pyinstaller打包成exe然后通过nssm(Non-Sucking Service Manager) 将其安装为系统服务。# 使用 PyInstaller 打包 pyinstaller --onefile --hidden-importwin32timezone --nameai_work_assistant main.py # 使用 nssm 安装服务 (在管理员命令行中) nssm install AIWorkAssistant C:\path\to\your\ai_work_assistant.exe这样工具就能在开机时自动启动在后台静默运行完全无感。4.2 日志清洗与聚合优化原始的窗口标题很“脏”。一个优化方向是在日志存入数据库前或生成报告前进行实时清洗。def clean_window_title(title, process_name): 清洗窗口标题提取核心信息 # 移除浏览器标签页中的网站名和浏览器名 if process_name chrome.exe or msedge.exe in process_name: # 匹配类似“文章标题 - 网站名 - 浏览器”的模式 import re match re.match(r^(.*?) - (.*?) - (?:Google Chrome|Microsoft Edge)$, title) if match: return match.group(1) # 只保留文章标题部分 # 移除IDE中的文件路径只保留文件名 if pycharm in process_name.lower() or vscode in process_name.lower(): # 假设标题格式为“文件名 - 项目路径 - IDE名” parts title.split( - ) if len(parts) 0: return parts[0] # 其他情况直接返回或做简单截断 return title[:80] # 统一截断到一定长度将这个清洗函数集成到log_activity中可以使得存入数据库的日志更干净为后续分析打下更好基础。4.3 提示词工程的迭代AI生成周报的质量90%取决于提示词。你需要不断调整你的提示词。例如加入角色设定“你是一位资深软件工程师的助理擅长从零散信息中提炼技术工作亮点。”指定输出格式“请用以下Markdown模板输出## 本周概述## 一、核心开发工作## 二、会议与协作...”提供例子“例如如果日志显示频繁访问某个技术文档网站和相关的IDE操作可以总结为‘完成了XXX模块的技术预研和原型搭建’。”控制风格“语言风格务实、精炼、使用主动语态、避免空洞形容词。”你可以将不同的提示词模板保存为文件让用户选择或根据季节、项目阶段自动切换。4.4 隐私与数据安全强化虽然数据在本地但为了万无一失可以增加加密功能。使用SQLite的加密扩展如sqlcipher或在数据落盘前进行简单的对称加密。更简单的方法是将数据库文件存放在受系统BitLocker或FileVault加密的磁盘上。5. 常见问题与实战排坑记录在实际开发和使用的几个月里我遇到了不少问题这里总结一下希望能帮你绕过这些坑。5.1 活动记录不准确或遗漏现象记录里出现了大量“未知”或“桌面”窗口或者在某些全屏应用游戏、视频播放器期间没有记录。排查权限问题确保程序以足够的权限运行。某些系统窗口需要管理员权限才能捕获。API限制GetForegroundWindow在某些全屏独占模式下会失效。这是系统层面的限制通常无解。但对于办公场景影响不大。快速切换如果记录间隔如30秒内窗口切换频繁会丢失中间状态。可以考虑缩短间隔到10秒但会增加数据量。更好的办法是记录“焦点切换事件”而不是轮询。Windows API提供了SetWinEventHook来监听EVENT_SYSTEM_FOREGROUND事件可以实现事件驱动的精确记录。这比轮询更高效、更准确但代码复杂度稍高。解决对于办公场景30秒轮询进程名过滤基本够用。如果追求极致精度可以研究事件钩子技术。5.2 Ollama模型响应慢或出错现象生成周报耗时过长超过2分钟或者返回乱码、无关内容。排查硬件资源首先检查CPU/内存/GPU使用率。7B/8B的模型在纯CPU上推理确实较慢可能需数分钟。如果拥有至少6GB显存的NVIDIA GPU确保Ollama使用了GPU加速运行ollama run时观察日志或使用ollama ps查看。提示词过长检查输入的日志文本是否过长。如果超过模型上下文长度会导致生成质量下降或失败。务必在代码中加入长度检查并进行截断或分段处理。模型未加载首次使用某个模型或长时间不用Ollama可能会卸载它。在生成任务开始时可以发送一个简单的测试请求如“你好”来“唤醒”模型。温度参数过高过高的temperature会导致输出随机、不连贯。对于总结任务务必调低0.1-0.3。解决硬件如果CPU太慢考虑使用更小的模型如TinyLlama(1.1B)它在总结任务上也有不错的表现。优化对日志进行预处理和压缩。例如将连续相同的“Visual Studio - coding.py”合并为一条并注明持续时间。将大量浏览同一网站如Stack Overflow的日志合并为“进行了约2小时关于XXX问题的技术调研”。这个预处理本身可以用一些简单的规则或一个更小的、专门训练的文本分类模型来完成从而大幅减少送入大模型的文本量。5.3 生成的周报内容空洞或分类错误现象AI生成的周报只是简单罗列了日志中的关键词没有归纳或者把写代码的时间错误归类为“会议沟通”。排查与解决强化提示词在提示词中更明确地给出分类定义和例子。例如“‘核心开发’指在IDE如VS Code, PyCharm中编写、调试代码或在终端运行命令。‘会议沟通’指使用腾讯会议、钉钉、微信工作群等软件进行同步交流...”提供上下文在发送给AI的日志中可以附加一些元信息。例如在每周日志的开头手动添加一行“本周主要项目XXX系统重构。重点关注模块用户认证与支付。” 这能引导AI围绕特定主题进行总结。后处理AI生成的结果不是最终答案。可以写一个简单的后处理脚本对某些明显的关键词进行替换和润色。或者采用“AI生成草稿 - 人工微调”的模式这本身也比从零写要快得多。混合规则与AI对于明确无误的分类先用规则处理。例如所有Teams.exe或wemeetapp.exe的进程直接标记为“会议”。将规则处理后的标签和原始日志一起交给AI让它专注于对模糊内容如浏览器标题的理解和归纳。5.4 数据库文件过大现象运行几个月后SQLite文件可能达到几百MB。解决定期清理旧数据。在周报生成任务中加入删除N周前数据的逻辑。def cleanup_old_logs(db_path, weeks_to_keep8): conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute(DELETE FROM activity_logs WHERE date(timestamp) date(\now\, ?), (f-{weeks_to_keep} weeks,)) deleted_rows cursor.rowcount conn.commit() conn.execute(VACUUM) # 收缩数据库文件回收空间 conn.close() print(f已清理{deleted_rows}条{weeks_to_keep}周前的日志记录。)这个由200多行代码编织而成的小工具已经安静地在我的电脑后台运行了数月。它从不打扰我却在我需要时递上一份脉络清晰的周报草稿。它可能不够完美分类有时会出错总结有时会遗漏细节但它解决了那个最核心的问题——将我从重复、低效的机械回忆中解放出来。我不再需要费力回想“周三下午到底干了啥”而是可以基于这份AI生成的草稿快速地进行补充、修正和升华把时间真正花在思考工作本身而不是记录工作上。技术的意义就在于此用一点点的自动化去换取那些更值得投入专注力的时刻。如果你也受困于周报不妨试试自己动手实现一个这个过程本身就是对“高效工作”的一次深刻实践。