每周五下午,团队群里开始催周报的时候,我基本都会对着屏幕发呆几分钟。写代码、开会、修bug忙了一周,真要回忆这周干了啥,脑子里那些片段全混在一起。翻Git记录、翻聊天记录、翻任务面板,最后憋出一段自己都不想看的流水账。后来我花几个晚上搭了一套基于AI Agent的周报自动化工作流,把收集、清洗、生成、推送全部串起来,现在周五只需要花几分钟审一遍生成结果,剩下的时间都省出来了。这篇博文就是我当时完整的搭建实录,代码全部贴出来。
我先把结论放在前面:周报这份工作,本质上是数据整理,不是写作。一旦你接受这个定位,就会发现它非常适合用AI Agent来跑。这篇文章适合被周报折腾的人、想统一团队周报格式的leader、以及想在真实业务场景里落地AI Agent但不想一上来就套重型框架的开发同学。下面是我从零开始的完整过程。
1. 周报自动化的真正痛点:不是缺文采,而是缺数据整理
1.1 周五下午三点的“黑暗时刻”
说句实话,写周报这件事本身没什么技术含量,但每个周五下午它总能准时让人烦躁。我观察过一圈身边的同事,大家周五的流程惊人的一致:先翻一遍Git提交记录,再去聊天软件里搜“这周干了啥”,运气好能翻到自己前两天留下的零散笔记,运气不好就全靠硬想。整个过程通常要花30到40分钟,写出来的内容不是流水账就是空泛的自我表扬,更难受的是,你辛辛苦苦写完了,leader可能只花30秒扫一眼,里面的重点还得他费劲帮你找。
后来我琢磨了一下,周报本质上是把一个周期内散落在不同地方的信息,提炼成几个有逻辑的模块。现实中这些输入源非常多样:Git提交信息、任务平台的状态、会议纪要、临时记录的Notes,甚至还有聊天记录里的关键结论。如果靠人工回忆,漏项是必然的;如果靠纯脚本把数据拼成一段话,又没有任何可读性。我当时的判断是:这件事最好由AI Agent来做——它既能自动化收集数据,又能用大模型把数据组织成结构清晰、读起来像正常人写的周报。
1.2 为什么这件事要交给AI Agent而不是模板
可能有人会问,市面上的周报模板工具已经很多了,为什么非要扯上AI Agent。我的回答有两个。
第一,周报的输入源是分散且不规则的。Git提交信息里中英文混杂,任务平台的状态不一定和实际同步,Notes里可能只有一两句含糊的感想。模板只能让你填一个又一个空,但没有办法替你去理解、归类、提炼这些非标准化数据。AI Agent能做这件事,因为它背后有大模型的理解能力,能够根据上下文把零散信息组织成“本周完成、进行中、风险问题、下周计划”这类结构。
第二,AI Agent的价值是跑完一个闭环,而不仅仅是提供一个聪明的Prompt。打个比方,Prompt像点菜,Agent是后厨帮你买菜、洗菜、切菜、炒菜。你只需要周五设置好定时任务,它自己去收集数据、生成周报、推送到群,你要做的只是最后看一眼。这也是我把方案从“一段Prompt”升级成“一个工作流”的根本原因。
2. 整体工作流设计:先画流程再写代码
2.1 拆解周报工作流的四个环节
动手写代码之前,我习惯先用纸把流程画出来。这个习惯是以前做数据平台时学到的:任何自动化任务,先别急着写逻辑,把数据从哪里来、到哪里去、中间经过哪些处理画清楚,后面能少踩一半坑。
我的周报自动化工作流拆成四个环节:
- 数据收集:从Git提交记录、Jira或Trello等任务平台、零散笔记里抓取一周的信息。
- 数据清洗:过滤掉merge commit、无意义的chore、不属于自己的提交,把原始数据整理成统一格式。
- AI生成:将清洗后的数据组装成上下文,交给大模型生成一份结构化的周报。
- 推送与归档:把最终结果推到飞书、钉钉或邮件,顺手存一份到本地目录或知识库。
为什么要按这四个环节拆?核心原因是“可替换”。今天你用的是Git提交数据,明天想把“本周上线清单”也加进来,只需要在数据收集环节增加一个函数;今天推送飞书,明天公司换了钉钉,只需要改最后一步。每个环节之间的接口是明确的——收集环节输出JSON,清洗环节输出干净的JSON,生成环节输出Markdown文本,推送环节拿到Markdown去发送。模块化带来的好处,在维护阶段会体现得特别明显,因为它降低了“改一处崩全局”的概率。
2.2 自研脚本还是可视化平台:一次真实的技术选型对比
画完流程之后,我面临一个很实际的问题:这套流程用Python脚本实现,还是用Dify、Coze(扣子)这类可视化工作流平台,或者用n8n这种集成工具?
我把三种方案摆在一起做了个对比。
| 方案 | 适合人群 | 优点 | 缺点 |
|---|---|---|---|
| Python + LLM SDK | 有编程基础的开发者 | 灵活、可控、能处理脏数据 | 需要自己写采集和清洗逻辑,维护成本高 |
| Dify / Coze 工作流 | 产品经理、运营、非程序员 | 可视化配置、上手快、内置常用节点 | 复杂清洗逻辑受限,数据源集成要另外配 |
| n8n + 自定义节点 | 偏后端、已有系统多 | 集成范围广,适合跨系统串联 | 对数据清洗这类“重逻辑”场景,可视化拖拽反而别扭 |
我当时的选择是:主体用Python + LLM SDK自研,同时用Dify搭了一个轻量版给不写代码的同事用。为什么这么选?因为Git提交记录是典型的“脏数据”重灾区——同一个人换邮箱会被识别成两个人,Chore提交混在功能提交里,还有大量依赖升级的噪音。这种清洗逻辑用可视化节点做起来非常别扭,但在Python里就是几行正则和函数的事。
不过后来我也发现,如果数据源本身就比较规整,比如团队统一用Jira并且状态更新及时,那确实没必要上代码,Dify一个LLM节点加几个前置节点就够了。所以“哪个方案最好”的判断标准,其实是“你手里的数据干不干净”。数据越规范,越适合可视化平台;数据越乱,越需要代码来做深度清洗。
2.3 为什么是Agent而不是一个Prompt模板
再说说“Agent”这个定语。我见过很多号称“AI写周报”的方案,本质上就是一个Prompt模板加一段输入文本,让模型总结一下。这种方案不是不能用,但它有两个致命问题。
第一,它不会替你去取数据。你得自己把Commit记录和任务列表复制粘贴进去,一旦数据源多了,操作反而更繁琐。第二,它对脏数据没有免疫力。输入里如果混了同事的名字、无意义的merge提交,输出必定跟着乱,而且你很难控制模型生成的边界。
而我说的Agent,是一个能“自动执行流程”的程序:按计划收集数据、过滤数据、理解数据、生成文本、推送结果。每一个环节都在代码里显式定义,出现问题时可以单独定位。严格讲,我这版并不是带自主规划循环的强Agent,更像一个轻量级Agent框架,但它的结构已经是Agent的雏形——有感知(数据采集)、有决策(Prompt中的判断规则)、有行动(推送和存档)。
这个定位挺重要的。因为很多人一听到Agent就想到ReAct循环、工具调用、记忆机制。如果你做周报这种规则明确的任务,完全不需要绕一圈。先跑通一个轻量闭环,比设计一个所谓的“华丽智能体”要实用得多。
3. 完整代码:五分钟看懂核心实现
3.1 项目目录和核心配置
我建议你在自己的机器上建一个独立目录,把下面的代码放进去跑。我实际使用的项目结构很简单,四个文件足以跑通整个流程:
weekly_report_agent/ ├── config.yaml # 个人配置:仓库路径、通知地址等 ├── main.py # 主程序,按流程依次执行 ├── requirements.txt # openai, requests, pyyaml └── output/ └── 2025-W02.md # 自动生成的周报markdown存档依赖只有三个库:openai、requests、pyyaml。环境变量里需要配好你的LLM API Key,具体变量名取决于你用的是什么服务。如果你接的是兼容OpenAI协议的服务,改一下配置里的base_url和model就行。
下面是config.yaml的一个示例。这个文件把所有需要频繁改动的内容都收拢在一起,避免频繁改代码。
llm: provider: openai model: gpt-4o-mini base_url: null # 如果你接的是兼容OpenAI协议的服务,在这里填地址 temperature: 0.3 max_tokens: 2000 collectors: git: repo_path: /path/to/your/project since_days: 7 my_emails: - yourname@example.com task: enabled: false source: jira url: https://your-jira.example.com project_key: YOUR_PROJECT notifier: channel: feishu_webhook webhook_url: https://open.feishu.cn/open-apis/bot/v2/hook/your-bot-token output_dir: ./output提示:所有密钥都通过环境变量或配置文件传入,代码里不要硬编码任何API Key和token。一旦你把脚本交到别人手上,硬编码的密钥就是安全隐患。
3.2 数据收集:从Git拉取本周提交
写数据收集逻辑时,我没有用GitPython,而是直接调用Git命令。原因很简单:这个脚本目标是轻量、零重依赖,Git命令本身已经足够稳定和通用,没必要为一个周报脚本引入额外依赖。实际项目里如果你已经在用GitPython,换掉也完全没问题,这里只是给一个最省心的路径。
下面是收集Git提交记录的完整函数。我会把提交哈希、作者名、作者邮箱、提交日期、提交信息全部取出来,后面清洗时用得上。
import subprocess from datetime import datetime, timedelta def collect_git_commits(repo_path: str, since_days: int) -> list[dict]: since = (datetime.now() - timedelta(days=since_days)).strftime("%Y-%m-%d") cmd = [ "git", "-C", repo_path, "log", "--since", since, "--pretty=format:%H|%an|%ae|%ad|%s", "--date=format:%Y-%m-%d %H:%M" ] result = subprocess.run(cmd, capture_output=True, text=True, check=True) commits = [] for line in result.stdout.strip().splitlines(): if not line: continue hash_, author_name, author_email, date, message = line.split("|", 4) commits.append({ "hash": hash_[:8], "author_name": author_name, "author_email": author_email, "date": date, "message": message }) return commits注意我在--pretty里额外加了%ae这个字段,也就是提交者的邮箱。很多人在这一步不提取邮箱,后面清洗时就会遇到“同一个人的不同邮箱被当成两个人”的问题。这个坑在第五部分我会详细讲。
3.3 数据清洗:把脏数据变成干净JSON
收集到的原始数据不能直接丢给大模型,否则模型会一本正经地把你同事的提交、依赖升级的commit、以及merge信息全部写进你的周报。清洗这一步我做了三件事:只保留自己的提交;过滤明显的噪音提交;对过长的英文message做截断处理。
NOISE_KEYWORDS = ["merge", "merge branch", "chore(release)", "bump version", "update lockfile", "generated by"] def clean_commits(commits: list[dict], my_emails: list[str]) -> list[dict]: emails = set(my_emails) filtered = [] for c in commits: if c["author_email"] not in emails: continue lower_msg = c["message"].lower() if any(k in lower_msg for k in NOISE_KEYWORDS): continue if len(c["message"]) > 80: c["message"] = c["message"][:80] + "..." filtered.append(c) return sorted(filtered, key=lambda x: x["date"])这版clean_commits虽然不复杂,但解决了80%的脏数据问题。如果你还想更精细,可以再按任务维度分组、给commit打标签,但那已经属于锦上添花了,先把基本盘稳住。
这里我要多说一句:过滤merge提交时,不要只匹配小写merge,因为很多提交信息是“Merge branch 'master' into dev”这种大小写混合的,我上面用的是in lower_msg,直接对整句话做小写判断,更稳。
3.4 上下文组装:给Agent一份“看得懂”的输入
清洗完的数据现在可以组装上下文了。组装的关键是“结构化”——不要把所有提交信息拼成一大段人话,而是以JSON格式交给模型。大模型收到数组和字段名的时候,对数据的理解远比你把它翻译成一段散文要准。
我在组装上下文之前还会做一次脱敏,把疑似IP和长token串替换成占位符,目的是防止内部信息被带出去。有人可能觉得多此一举,但周报是会被转发到各种群里的,小心驶得万年船。
import re def redact(text: str) -> str: text = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "[IP]", text) text = re.sub(r"[A-Za-z0-9_\-]{24,}", "[TOKEN]", text) return text def build_context(commits: list[dict], tasks_done: list[str], tasks_todo: list[str], notes: list[str]) -> str: ctx = { "commits": [redact(c["message"]) for c in commits], "tasks_done": tasks_done, "tasks_todo": tasks_todo, "notes": [redact(n) for n in notes], } return json.dumps(ctx, ensure_ascii=False, indent=2)这条redact函数是我后期才加上的。第一次跑通时没做脱敏,生成结果里差点把内网IP发进群,后怕了很久。如果你的工程里涉及数据库连接串、内部域名,建议把这套脱敏规则扩展得更严一点。
3.5 核心生成逻辑:调用LLM并约束输出格式
到核心步骤了。我直接调用LLM接口,传入一个经过三轮迭代才稳定下来的系统提示词。这个提示词很关键,它决定了生成内容的质量边界。
SYSTEM_PROMPT = """你是一名严谨的软件工程师,正在编写个人周报。 要求: 1. 周报分四段:本周完成、进行中、风险与问题、下周计划。 2. 每个任务用一句话概括,务必写出进展或结果。 3. 只能使用用户提供的上下文信息,不得编造数据或任务。 4. 语气简洁、书面,避免“非常努力”“积极推动”“赋能”等空话套话。 5. 如果没有执行中的任务,可以省略“进行中”段落。 输出为Markdown格式。 """ def generate_report(context: str, llm_config: dict) -> str: client = OpenAI() resp = client.chat.completions.create( model=llm_config.get("model", "gpt-4o-mini"), temperature=llm_config.get("temperature", 0.3), max_tokens=llm_config.get("max_tokens", 2000), messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"本周上下文数据如下:\n{context}\n\n请生成周报。"} ] ) return resp.choices[0].message.content如果你接的是兼容OpenAI协议的服务,只需要在创建OpenAI客户端时传base_url参数。其余接口结构都一样,不需要绑定某一家。“能跑通”比“选型完美”重要,这是我个人的体会。
3.6 推送与存档:发飞书Webhook并保存Markdown
周报生成后,如果只是打印到终端,那就没达到“自动闭环”的目的。我自己用的是飞书自定义机器人Webhook,简单直接,普通文本消息就够了。钉钉、企业微信的Webhook格式大同小异,照官方文档改一下payload字段即可。
def send_feishu_webhook(url: str, content: str) -> None: payload = { "msg_type": "text", "content": {"text": content} } resp = requests.post(url, json=payload, timeout=10) resp.raise_for_status() def save_report(report: str, output_dir: str, filename: str) -> str: from pathlib import Path Path(output_dir).mkdir(parents=True, exist_ok=True) path = Path(output_dir) / filename path.write_text(report, encoding="utf-8") return str(path)主程序再把这几步串起来,整体形式就是一个典型的流程编排:
def main(): cfg = load_config("config.yaml") commits = collect_git_commits( cfg["collectors"]["git"]["repo_path"], cfg["collectors"]["git"].get("since_days", 7), ) commits = clean_commits(commits, cfg["collectors"]["git"]["my_emails"]) context = build_context(commits, tasks_done=[], tasks_todo=[], notes=[]) report = generate_report(context, cfg["llm"]) save_report(report, cfg["output_dir"], f"{datetime.now():%Y-W%W}.md") send_feishu_webhook(cfg["notifier"]["webhook_url"], report)tasks_done和tasks_todo在示例里是空的。如果你的任务平台有API,就自己去写一个collect_tasks()函数,把任务状态转成列表传进来。整体结构是开放的,不限定数据源。
4. 关键参数与Prompt工程:让输出变成“像人写的”
4.1 我的三版Prompt迭代过程
直接给一版能用的Prompt不算本事,我更想把踩坑过程写出来。你会发现,看起来差不多的Prompt,输出质量能差出一条街。
第一版,我写的是“请根据以下数据生成周报”。结果输出非常空洞,模型把每条commit差不多复述了一遍,最后加一句“本周工作较为饱满,下周继续努力”,AI味重到根本不敢发出去。
第二版,我加上了“分四段:本周完成、进行中、风险与问题、下周计划”的结构要求。效果好了很多,至少能看了,但还是偶尔出现“存在一定风险,需要持续关注”这种正确的废话。
第三版,我加入了“只能使用用户提供的上下文信息,不得编造”和“避免空话套话”,同时明确“每个任务用一句话概括,写出进展或结果”。这一版基本就稳定了,输出风格很像一个正常工程师写的周报。
总的来说,Prompt迭代的顺序应该是:先定结构,再控事实,最后磨语气。你要是反着来,先追求语言优美,结果结构一团糟,最后又要推倒重来。
4.2 temperature、max_tokens到底怎么调
这两个参数是影响输出的最直接因素。
temperature控制随机性。周报是事实型文本,我建议设置在0.2到0.4之间。我实测0.3最适合:既不会像机器一样干巴巴,也不会放飞自我。如果你设置在0.7以上,很容易出现“把并发压测写成了性能优化方案”这种夸大其词的句子,因为模型在创作而不是陈述。
max_tokens要估算周报长度。一般周报800到1500字就够用,2000个token已经覆盖英文和中文混合场景。设太大会浪费等待时间,设太小会截断Markdown表格,两头不讨好。如果你每周任务特别多,再适当加。
还有一个容易被忽略的参数是frequency_penalty。如果你发现自己生成的周报每段开头总是“本周”“该项目”之类的词重复,可以适当调高frequency_penalty,比如0.3到0.5,让输出用语更多样一点。
4.3 上下文太长时的截断策略
如果你一周的提交特别多,上下文会越攒越长,token消费和响应速度都跟着上来。大模型对长上下文虽然能处理,但周报不需要模型读6000个词的commit日志。
我的策略很简单:清洗完数据后,对列表做“按时间倒序+截断”,优先保留最近的、最关键的提交。你也可以增加filter_by_path之类的过滤函数,只保留src目录下的改动,把文档、配置类的噪音挡在外面。
def truncate_messages(messages: list[str], max_len: int = 3000) -> list[str]: total = 0 result = [] for m in reversed(messages): total += len(m) if total > max_len: break result.append(m) return list(reversed(result))这个截断逻辑虽然粗暴,但非常实用。只要你能接受“更早的提交细节可能丢失”,这个策略就能帮你稳定控制成本。
5. 实战问题与排查技巧:跑了半年的经验和坑
5.1 同一人多个Git邮箱,周报被撕成两半
第一次跑通全流程后,我得意地把周报发给leader,结果发现“自己”的提交只显示了一半。排查后才发现,我的Git全局配置和项目级配置用了不同的邮箱,Git把它们记成了两个作者。更要命的是,有一个同事的提交也混进来了,因为他在某些自动化任务里用了另一个邮箱后缀。
解决方案就是我在清洗函数里已经做的事:维护一个my_emails列表,把所有属于你的邮箱都列进去,清洗时按邮箱过滤,不要按作者名过滤。作者名是可以改的,但邮箱在绝大多数情况下是稳定的,用邮箱做过滤更可靠。
5.2 周报里出现了我没做过的事:模型的“善解人意”
有一次生成的周报里写着“完成支付模块重构,性能提升30%”,我看了一愣:我一周做的明明只是修了一个支付回调的bug,哪来的重构?后来把上下文拉出来查,发现那个commit信息写得太像一次重构,模型顺着就展开了想象。
这个问题的根源不在模型,而在输入数据的表述。我的解决办法是双管齐下:在Prompt里强调“不得编造数据”,同时在数据清洗阶段增加过滤,把“refactor”“optimize”这类容易被模型放大的提交信息原样保留,不去做过多的解释。其实最有效的是“人工审核兜底”,每周推送前我会花一分钟扫一遍,发现问题立刻改。Agent能帮你省时间,但不能完全替你做判断。
5.3 定时任务跑挂了,但是没人知道
给脚本加crontab之后,我一度非常放心,直到第二周才发现webhook推送失败了两天。原因是crontab的运行环境是一个最小PATH,python3和git都可能找不到,而且脚本遇到异常会直接退出,不给我留任何日志。
解决方式:
- 用绝对路径指定可执行文件,例如
/usr/bin/python3 - 在脚本入口包一层 try/except,把异常写入日志文件
- crontab里重定向标准输出和标准错误
下面是我实际在用的定时任务配置:
0 18 * * 5 /usr/bin/python3 /home/yourname/weekly_report_agent/main.py >> /var/log/weekly_report_agent.log 2>&1还有一个容易被忽略的细节:不要把脚本放在会被Git管理的目录下面,否则脚本自己的输出文件每次都会成为commit对象,下次数据清洗时还得想方设法把自己排除掉。我在第二周就踩了这个坑,后来立刻把脚本和项目仓库分开了。
5.4 脱敏不彻底,内部信息差点被发进群
周报送进群之前,我把脱敏函数加进了上下文组装环节。但第一版脱敏只过滤了IPv4地址,没过滤内网域名和长token串。有一次生成结果里出现了一个疑似数据库连接串,长度很长、包含下划线和等号,差点被推送到群里。
后来我把脱敏规则扩展成三段:IP地址、长度超过24位的连续字符、以及形如http://内网主机名/...的URL。虽然这会误伤一些正常的内部项目名,但周报这种场景,牺牲一点可读性换安全是值得的。尤其是团队群里有外部协作者时,这类问题一旦发生就是事故。
6. 方案扩展与我的真实体会
6.1 从代码方案迁移到Dify、Coze的轻量版
我前面提到给不写代码的同事搭了一个Dify版。如果你也不想维护代码,可以参考这个思路:在Dify里创建一个“文本生成”类型的工作流,输入节点接收原始数据,中间接几个清洗节点,最后用LLM节点按我上面的系统提示词生成周报。Coze(扣子)也类似,把数据源节点和LLM节点连线就行。
推荐做法是:代码方案作为自己的主力跑,因为数据清洗灵活;团队成员就用低代码版,因为大家要的是“把内容贴进去就出结果”,不在乎过程。如果你连低代码平台都不想配,还有一个更轻的方案:把数据导成CSV,直接丢给我上面这个Python脚本,一样能生成。
6.2 我为什么不建议完全去掉人工审核
看到这里你可能觉得,既然Agent已经能把周报生成得这么流畅,为什么还要人工审核?我的回答是:周报是为了给决策者看,不是写给档案馆存档的。自动生成的内容可以做到结构正确、语言通顺,但Agent不知道leader最近在关心什么。有些本周踩到的坑、下周的潜在风险、某个客户的关键反馈,藏在commit信息和任务状态之外,Agent看不到。
所以我一直把这套方案定位成“半自动”:Agent负责把80%的重复劳动做掉,我只需要花几分钟补齐那20%的“人味”。这也是我认为最健康的人机协作姿势——不是让机器完全替代人,而是让它把人从低价值重复劳动里解放出来,让你有精力去做只有人能做的判断。
6.3 跑顺之后可以继续扩展的方向
这套流程真正跑顺之后,往后面加东西是比较容易的,因为核心的四个环节都解耦了。
比如增加知识库沉淀:每次生成的周报自动归档到Notion或本地数据库,按月搜索,方便复盘。再比如增加趋势分析:让Agent对比最近四周的周报,自动统计任务完成率、延期项,为排期提供参考。还可以接入更多数据源:从飞书云文档、多维表格、语雀文档里读取任务信息,动态构建上下文。只要数据收集、清洗、生成、推送这四个环节的接口保持不变,你想加多少扩展都很容易。
最后再分享一个小技巧:这套流程跑顺之后,你会在周五之前主动把数据源维护好,因为你知道提前维护能生成一份更准确的周报。周报自动化这件事,表面上是在减少写周报的时间,实际上是在倒逼你把工作数据沉淀成结构化的东西。对我个人来说,这个收益远大于那每周省下来的半小时。