每到周五,我最不想做的一件事就是写周报。
不是因为这件事有多难,而是因为它很烦。
这一周做了什么,临到周五经常想不起来。打开聊天记录、任务列表、代码提交、会议纪要,东一块西一块。最后写出来的周报也很容易变成流水账:
本周完成接口开发。
本周参与项目沟通。
本周修复若干问题。
本周继续推进需求。
这种周报不能说错,但没什么信息量。老板看不出你做了什么,自己也沉淀不下来。
所以我做了一个小工具:把一周的零散记录丢进去,它自动帮我整理成一份像样的周报。
这篇文章会从零开始实现一个周报生成器。
你看完之后,可以得到一个本地网页工具。打开网页,输入这周做过的事情,点击按钮,就能生成一份结构化周报。
它支持这些功能:
输入一周零散工作记录
选择周报风格
填写岗位、部门、汇报对象
自动生成本周完成、关键成果、问题风险、下周计划
支持复制和下载 Markdown 文件
如果本地大模型没连上,也会生成一个基础版周报,不至于整个程序不能用
这个项目还是使用 Python 和 Streamlit 来做界面,用 Ollama 调用本地大模型。这样不需要写前端,也不用把工作记录发到外部平台。
先看一下效果
假设我输入这些记录:
周一:完成用户登录接口重构,顺便把异常处理统一了一下。周二:和产品讨论了数据看板需求,确认第一版先做用户增长、活跃、留存三个模块。周三:修复线上导出 Excel 偶发失败的问题,原因是部分字段为空时没有做兼容。周四:写了数据看板接口文档,和前端对了一遍字段。周五:完成用户增长模块接口开发,留存模块还差一部分 SQL 没写完。问题:测试环境数据不完整,部分指标暂时无法验证。下周:继续完成留存模块,补充单元测试,推动前端联调。
生成出来的周报大概是这样:
# 本周工作周报## 一、本周完成1. 完成用户登录接口重构,并统一了异常处理逻辑,提高了接口稳定性和后续维护效率。2. 参与数据看板需求讨论,明确第一版范围,包括用户增长、活跃、留存三个核心模块。3. 修复线上 Excel 导出偶发失败问题,补充了空字段兼容处理。4. 完成数据看板接口文档编写,并与前端完成字段对齐。5. 完成用户增长模块接口开发,留存模块开发仍在推进中。## 二、关键成果本周主要围绕登录接口优化和数据看板需求推进展开。登录接口重构已经完成,线上导出问题也已修复。数据看板方向完成了需求确认、接口文档和部分模块开发,为后续联调打下了基础。## 三、问题与风险1. 测试环境数据不完整,部分指标暂时无法充分验证。2. 留存模块仍有部分 SQL 未完成,可能影响后续联调节奏。## 四、下周计划1. 完成留存模块相关 SQL 和接口开发。2. 补充核心接口单元测试。3. 推动数据看板前后端联调。4. 根据测试结果继续完善异常场景处理。
这比自己临时憋出来的流水账要好很多。
重点不是它写得多漂亮,而是它能把零散事项整理成一份结构清楚、重点明确的周报。
这个工具的实现思路
这个周报生成器的核心流程很简单。
用户输入一周工作记录。
程序把岗位、部门、周报风格、补充要求拼成一个提示词。
把提示词交给本地大模型。
模型生成周报。
页面展示结果,并支持下载。
除了调用模型,我还加了一个兜底逻辑。如果 Ollama 没有启动,程序会用简单规则生成一份基础版周报。这个基础版没有大模型写得自然,但能保证工具不会直接报错。
也就是说,这个项目不只是一个 API 调用壳子,而是一个能正常使用的小应用。
准备环境
我这里用的是 Python 3.10。
新建项目文件夹:
weekly_report_generator
进入项目:
cd weekly_report_generator
创建虚拟环境:
python -m venv .venv
macOS 或 Linux 激活虚拟环境:
source .venv/bin/activate
Windows 激活虚拟环境:
.venv\Scripts\activate
新建 requirements.txt:
streamlitrequests
安装依赖:
pip install -r requirements.txt
这个项目依赖很少。Streamlit 用来做网页界面,requests 用来请求本地 Ollama 服务。
然后准备本地大模型。
如果你已经安装过 Ollama,可以直接拉一个中文效果不错的模型。这里仍然用 qwen2.5:7b 举例:
ollama pull qwen2.5:7b
启动测试:
ollama run qwen2.5:7b
如果电脑配置一般,可以用小一点的模型:
ollama pull qwen2.5:3b
只要模型能正常对话,后面的代码就能调用它。
项目结构
项目结构非常简单:
weekly_report_generator├── app.py└── requirements.txt
下面直接给出完整代码。
完整代码
新建 app.py,把下面代码复制进去。
import reimport datetimeimport requestsimport streamlit as stdefclean_text(text: str) -> str: text = text.replace("\x00", "") text = re.sub(r"[ \t]+", " ", text) text = re.sub(r"\n{3,}", "\n\n", text)return text.strip()defsplit_work_items(text: str) -> list[str]: text = clean_text(text) lines = text.splitlines() items = [] buffer = []for line in lines: line = line.strip()ifnot line:continue line = re.sub(r"^[\-\*\d\.、\s]+", "", line).strip()if line: buffer.append(line)for item in buffer:if len(item) <= 120: items.append(item)else: parts = re.split(r"[。;;]", item)for part in parts: part = part.strip()if part: items.append(part)return itemsdefguess_item_type(item: str) -> str: problem_words = ["问题", "风险", "阻塞", "延期", "失败", "异常", "bug","报错", "卡住", "依赖", "不完整", "无法", "缺少" ] next_words = ["下周", "计划", "继续", "准备", "推进", "跟进","待", "todo", "TODO", "后续" ] meeting_words = ["会议", "沟通", "讨论", "评审", "同步", "对齐", "确认" ] result_words = ["完成", "上线", "发布", "交付", "优化", "修复", "实现","开发", "整理", "分析", "撰写", "测试", "重构", "接入" ] lower_item = item.lower()if any(word.lower() in lower_item for word in problem_words):return"problem"if any(word.lower() in lower_item for word in next_words):return"next"if any(word.lower() in lower_item for word in meeting_words):return"communication"if any(word.lower() in lower_item for word in result_words):return"done"return"other"defbuild_fallback_report( raw_records: str, user_name: str, role: str, department: str, week_range: str) -> str: items = split_work_items(raw_records) done_items = [] communication_items = [] problem_items = [] next_items = [] other_items = []for item in items: item_type = guess_item_type(item)if item_type == "done": done_items.append(item)elif item_type == "communication": communication_items.append(item)elif item_type == "problem": problem_items.append(item)elif item_type == "next": next_items.append(item)else: other_items.append(item)if other_items: done_items.extend(other_items) name_line = f"- 汇报人:{user_name}"if user_name else"" role_line = f"- 岗位:{role}"if role else"" department_line = f"- 部门:{department}"if department else"" week_line = f"- 周期:{week_range}"if week_range else"" header_info = "\n".join( line for line in [name_line, role_line, department_line, week_line] if line )defmake_numbered_list(list_items: list[str], empty_text: str) -> str:ifnot list_items:return empty_text result = []for index, item in enumerate(list_items, start=1): result.append(f"{index}. {item}")return"\n".join(result) done_text = make_numbered_list(done_items, "暂无明确完成事项。") communication_text = make_numbered_list(communication_items, "暂无明确沟通协作事项。") problem_text = make_numbered_list(problem_items, "暂无明确问题或风险。") next_text = make_numbered_list(next_items, "暂无明确下周计划。") summary = "本周主要围绕既定工作事项推进,完成了部分开发、沟通、修复或整理工作。后续需要继续关注未完成事项和潜在风险。" report = f"""# 本周工作周报{header_info}## 一、本周完成{done_text}## 二、沟通与协作{communication_text}## 三、关键成果{summary}## 四、问题与风险{problem_text}## 五、下周计划{next_text}"""return report.strip()defbuild_prompt( raw_records: str, user_name: str, role: str, department: str, week_range: str, report_style: str, report_length: str, manager_focus: str, extra_requirements: str) -> str: prompt = f"""你是一名经验丰富的职场周报整理助手。请根据用户提供的一周工作记录,整理成一份可以直接发给上级的中文周报。写作要求:1. 只根据用户提供的记录整理,不要编造没有出现过的项目、指标、结论。2. 不要写得太浮夸,也不要像宣传稿。3. 不要简单复制原文,要把零散记录整理成清楚、自然、专业的表达。4. 如果某一部分信息不足,可以写“暂无明确记录”,不要硬编。5. 尽量体现工作价值,比如完成了什么、解决了什么问题、推进了什么事项。6. 周报要适合真实职场场景,表达自然,不要有明显模板味。基本信息:汇报人:{user_name or"未填写"}岗位:{role or"未填写"}部门:{department or"未填写"}周期:{week_range or"未填写"}周报风格:{report_style}篇幅要求:{report_length}上级关注点:{manager_focus or"未填写"}补充要求:{extra_requirements or"无"}用户的一周工作记录如下:{raw_records}请按下面结构输出:# 本周工作周报## 一、本周完成用条目列出本周完成的主要事项。不要写流水账,要适当合并同类项。## 二、关键成果总结本周最有价值的成果,说明这些工作带来了什么进展。## 三、问题与风险整理本周遇到的问题、风险、阻塞点。如果没有明确记录,就写暂无明确风险。## 四、下周计划根据用户记录中提到的后续事项,整理下周计划。如果没有明确记录,可以根据未完成事项做谨慎延伸,但不要过度发挥。## 五、需要支持如果记录中出现依赖、阻塞、资源不足等情况,请写出来。否则写暂无。请直接输出周报正文,不要解释你的写作过程。""".strip()return promptdefask_ollama( prompt: str, model: str, host: str) -> tuple[bool, str]: url = f"{host.rstrip('/')}/api/chat" payload = {"model": model,"stream": False,"messages": [ {"role": "system","content": "你是一个严谨、自然、专业的中文职场写作助手。" }, {"role": "user","content": prompt } ] }try: response = requests.post(url, json=payload, timeout=180) response.raise_for_status() data = response.json() answer = data["message"]["content"].strip()returnTrue, answerexcept requests.exceptions.ConnectionError:returnFalse, "无法连接到 Ollama。请确认 Ollama 已经启动。"except requests.exceptions.Timeout:returnFalse, "模型响应超时。可以换一个更小的模型,或者减少输入内容。"except Exception as e:returnFalse, f"调用模型时出错:{e}"defget_default_week_range() -> str: today = datetime.date.today() monday = today - datetime.timedelta(days=today.weekday()) friday = monday + datetime.timedelta(days=4)returnf"{monday.strftime('%Y-%m-%d')} 至 {friday.strftime('%Y-%m-%d')}"defbuild_markdown_file_name(week_range: str) -> str: safe_name = re.sub(r"[^\d\-至_]+", "_", week_range)returnf"weekly_report_{safe_name}.md"defmain(): st.set_page_config( page_title="周报生成器", page_icon="📝", layout="wide" ) st.title("周报生成器") st.caption("把一周零散记录整理成一份能直接发出去的周报。")with st.sidebar: st.header("模型设置") use_llm = st.checkbox("使用本地大模型生成", value=True ) model_name = st.text_input("Ollama 模型名", value="qwen2.5:7b" ) ollama_host = st.text_input("Ollama 地址", value="http://localhost:11434" ) st.markdown("---") st.header("周报设置") report_style = st.selectbox("周报风格", options=["正式简洁","偏成果导向","偏技术细节","适合发给直属领导","适合团队内部同步" ], index=0 ) report_length = st.selectbox("篇幅", options=["中等篇幅","简短一点","详细一点" ], index=0 ) left_col, right_col = st.columns([1, 1])with left_col: st.subheader("基本信息") user_name = st.text_input("汇报人", placeholder="可以不填" ) role = st.text_input("岗位", placeholder="比如:后端开发工程师、数据分析师、算法工程师" ) department = st.text_input("部门", placeholder="比如:技术部、数据部、增长组" ) week_range = st.text_input("周报周期", value=get_default_week_range() ) manager_focus = st.text_area("上级关注点", placeholder="比如:项目进度、风险、上线时间、数据结果。可以不填。", height=90 ) extra_requirements = st.text_area("补充要求", placeholder="比如:不要太长,突出数据看板项目,语气自然一点。可以不填。", height=90 )with right_col: st.subheader("本周工作记录") sample_text = """周一:完成用户登录接口重构,顺便把异常处理统一了一下。周二:和产品讨论了数据看板需求,确认第一版先做用户增长、活跃、留存三个模块。周三:修复线上导出 Excel 偶发失败的问题,原因是部分字段为空时没有做兼容。周四:写了数据看板接口文档,和前端对了一遍字段。周五:完成用户增长模块接口开发,留存模块还差一部分 SQL 没写完。问题:测试环境数据不完整,部分指标暂时无法验证。下周:继续完成留存模块,补充单元测试,推动前端联调。""" raw_records = st.text_area("把这一周做过的事粘贴到这里", value=sample_text, height=360 ) generate_button = st.button("生成周报", type="primary")if generate_button: raw_records = clean_text(raw_records)ifnot raw_records: st.warning("请先输入本周工作记录。")returnif use_llm: prompt = build_prompt( raw_records=raw_records, user_name=user_name, role=role, department=department, week_range=week_range, report_style=report_style, report_length=report_length, manager_focus=manager_focus, extra_requirements=extra_requirements )with st.spinner("正在生成周报..."): success, result = ask_ollama( prompt=prompt, model=model_name, host=ollama_host )if success: report = result st.success("周报已生成。")else: st.warning(result) st.info("已自动切换为基础规则生成版本。") report = build_fallback_report( raw_records=raw_records, user_name=user_name, role=role, department=department, week_range=week_range )else: report = build_fallback_report( raw_records=raw_records, user_name=user_name, role=role, department=department, week_range=week_range ) st.success("基础版周报已生成。") st.subheader("生成结果") st.markdown(report) st.download_button( label="下载 Markdown 文件", data=report.encode("utf-8"), file_name=build_markdown_file_name(week_range), mime="text/markdown" )with st.expander("查看 Markdown 原文"): st.code(report, language="markdown")if __name__ == "__main__": main()
运行项目
在项目目录下执行:
streamlit run app.py
打开浏览器后,你会看到一个本地网页。
左边填写基本信息,比如岗位、部门、汇报周期、上级关注点。
右边输入本周工作记录。
点击生成周报,程序就会整理出一份完整周报。
如果你没有启动 Ollama,页面会提示无法连接,并自动切换成基础版生成结果。这样做的好处是,读者第一次运行时就算模型没配好,也能看到工具效果。
代码拆解
这个项目的核心代码不长,我们分几块看。
第一块是清洗输入。
defclean_text(text: str) -> str: text = text.replace("\x00", "") text = re.sub(r"[ \t]+", " ", text) text = re.sub(r"\n{3,}", "\n\n", text)return text.strip()
用户输入的内容可能是从飞书、钉钉、微信、备忘录里复制来的,经常会有多余空格、空行和奇怪字符。
所以第一步先把文本简单清理一下。
这里没做特别复杂的处理,因为周报记录本来就不是标准数据。过度清洗反而可能把有用信息删掉。
第二块是把工作记录切成条目。
defsplit_work_items(text: str) -> list[str]: text = clean_text(text) lines = text.splitlines() items = [] buffer = []for line in lines: line = line.strip()ifnot line:continue line = re.sub(r"^[\-\*\d\.、\s]+", "", line).strip()if line: buffer.append(line)for item in buffer:if len(item) <= 120: items.append(item)else: parts = re.split(r"[。;;]", item)for part in parts: part = part.strip()if part: items.append(part)return items
这段代码主要给基础版周报使用。
如果用户输入的是一行一件事,就直接保留。
如果某一行太长,就按句号、分号切开。
虽然这个逻辑很简单,但够用了。周报生成不需要像 NLP 论文那样精细,只要能把零散事项拆成大致可处理的条目就行。
第三块是判断每条记录属于哪类。
defguess_item_type(item: str) -> str: problem_words = ["问题", "风险", "阻塞", "延期", "失败", "异常", "bug","报错", "卡住", "依赖", "不完整", "无法", "缺少" ] next_words = ["下周", "计划", "继续", "准备", "推进", "跟进","待", "todo", "TODO", "后续" ] meeting_words = ["会议", "沟通", "讨论", "评审", "同步", "对齐", "确认" ] result_words = ["完成", "上线", "发布", "交付", "优化", "修复", "实现","开发", "整理", "分析", "撰写", "测试", "重构", "接入" ]
这不是机器学习,只是关键词规则。
比如包含完成、修复、开发、优化,就更可能是本周完成事项。
包含问题、风险、阻塞、无法,就更可能是问题风险。
包含下周、计划、继续、待,就更可能是下周计划。
这个规则肯定不完美,但它有两个作用。
第一,如果本地大模型没有启动,仍然可以生成一份可用的基础周报。
第二,它让整个项目更完整,不完全依赖模型。
我写这种小工具时,一般都会保留一个兜底方案。真实使用中,模型服务总可能出现问题,程序最好不要一出错就完全不能用。
第四块是基础版周报生成。
defbuild_fallback_report( raw_records: str, user_name: str, role: str, department: str, week_range: str) -> str:
这个函数会根据上面的分类结果,把内容放进固定结构里。
它生成的周报没有大模型自然,但格式是完整的。
# 本周工作周报## 一、本周完成## 二、沟通与协作## 三、关键成果## 四、问题与风险## 五、下周计划
这个基础版本适合在没有模型时应急,也适合做对比。你会发现,大模型真正有价值的地方,不是凭空写内容,而是把原始记录变得更自然、更有层次。
第五块是提示词。
defbuild_prompt( raw_records: str, user_name: str, role: str, department: str, week_range: str, report_style: str, report_length: str, manager_focus: str, extra_requirements: str) -> str:
周报生成器好不好用,很大程度取决于提示词。
这里我没有只写一句请帮我生成周报,而是把约束写清楚。
比如:
只根据用户提供的记录整理,不要编造没有出现过的项目、指标、结论。不要写得太浮夸,也不要像宣传稿。不要简单复制原文,要把零散记录整理成清楚、自然、专业的表达。如果某一部分信息不足,可以写“暂无明确记录”,不要硬编。
这些约束很重要。
如果不加限制,模型很容易把周报写得特别虚。看起来很高级,实际上全是套话。
比如原始记录只是写了修复导出问题,它可能会扩展成显著提升系统稳定性,全面增强用户体验,有力支撑业务增长。
这种话不是不能写,但如果没有依据,就容易显得油。
所以这个工具的目标不是把周报写得漂亮,而是写得真实、清楚、能交付。
第六块是调用 Ollama。
defask_ollama( prompt: str, model: str, host: str) -> tuple[bool, str]:
这里请求的是本地接口:
url = f"{host.rstrip('/')}/api/chat"
payload 里有模型名、是否流式输出、system 消息和 user 消息。
payload = {"model": model,"stream": False,"messages": [ {"role": "system","content": "你是一个严谨、自然、专业的中文职场写作助手。" }, {"role": "user","content": prompt } ]}
stream=False 表示一次性返回完整结果。这样代码更简单。
如果想做得更像聊天应用,可以改成流式输出,让内容一段段显示出来。不过这篇先不展开,先把最小可用版本跑通。
为什么要用本地大模型
周报里经常包含真实工作内容。
如果只是测试,随便用什么都可以。但如果真的把它当工具用,我更倾向于本地运行。至少在这个项目里,数据不会发到外部平台。
当然,本地模型也有代价。
电脑配置差一点时,生成速度会慢。
小模型对复杂表达的处理不如大模型。
模型本身仍然可能理解错,所以生成后必须人工检查。
这也是我对这类工具的基本态度:它不是替你写完周报,而是帮你整理初稿。最终发出去之前,还是要自己看一遍。
怎么写输入记录,生成效果更好
这个工具不是魔法。输入质量越好,生成的周报越好。
我建议平时不要等到周五再回忆,而是每天随手记两三行。
比如这样记就够了:
周一:完成用户登录接口重构,统一异常处理。周二:和产品确认数据看板第一版范围,包括用户增长、活跃、留存。周三:修复线上 Excel 导出偶发失败问题,补充空字段兼容。周四:完成接口文档,并和前端对齐字段。周五:完成用户增长模块接口,留存模块 SQL 还在写。问题:测试环境数据不完整,部分指标无法验证。下周:完成留存模块,补测试,推动联调。
这种记录不用很正式。只要把事情说清楚,模型就能整理。
但如果输入是这样:
本周继续推进项目。处理了一些问题。参加了一些会议。下周继续。
那生成结果一定会很空。
因为模型没有足够的信息,只能写套话。
我自己比较推荐用这几个维度记录:
做了什么
做到什么程度
解决了什么问题
有没有阻塞
下周继续什么
有没有需要别人支持的地方
只要每天花两分钟记一下,周五生成周报时会轻松很多。
可以给工具加哪些功能
这个版本已经可以用了,但还比较基础。
如果要继续升级,我会优先加这几个功能。
第一个是从 Git 提交记录生成周报。
对程序员来说,很多工作其实都在 commit 里。可以用 Python 读取最近一周的 Git log,然后自动整理成开发记录。
比如执行:
git log --since="1 week ago" --pretty=format:"%ad %s" --date=short
拿到提交记录后,再交给模型整理。
这样就不用完全靠手写记录了。
第二个是接入任务管理工具。
如果团队用 Jira、禅道、飞书项目、TAPD,很多任务状态本来就在那里。后面可以通过 API 拉取本周完成的任务、进行中的任务和延期任务。
第三个是生成不同版本。
同一份工作记录,可以生成三个版本:
发给直属领导的版本
团队内部同步版本
个人复盘版本
这三个版本的重点是不一样的。
直属领导更关心进度、风险和结果。
团队内部更关心协作、依赖和排期。
个人复盘更关心经验、问题和改进。
第四个是保留历史周报。
现在生成结果只是下载成 Markdown。后面可以保存到本地数据库,比如 SQLite。这样每周的周报都能沉淀下来。
时间长了以后,还可以自动生成月报、季度总结、年终总结。
第五个是加一个润色模式。
很多人不是不会写周报,而是已经写了一版,但觉得不够清楚。
所以可以加一个模式:用户输入已有周报,模型只负责优化表达,不改变事实。
这个功能会非常实用。
一个更适合程序员的版本
如果你是程序员,可以在输入记录时多写一点技术细节。
比如不要只写:
修复导出问题。
可以写成:
修复线上 Excel 导出偶发失败问题,原因是部分字段为空时导出逻辑没有做兼容,已补充空值处理并验证通过。
不要只写:
完成接口开发。
可以写成:
完成用户增长模块接口开发,包括新增用户、活跃用户、渠道来源三个指标,已和前端完成字段对齐。
这样生成出来的周报会更有价值。
技术人的周报最怕两种情况。
一种是写得太细,像提交记录,领导看不出重点。
另一种是写得太虚,只剩推进、支持、优化、协作。
比较好的状态是:既能看出你做了具体事情,又能看出这些事情对项目有什么影响。
这个工具就是帮你从零散技术记录里提炼出这层表达。
这个项目真正有用的地方
周报生成器看起来是一个小工具,但它背后其实是很多 AI 应用都会遇到的问题。
第一,用户输入往往是乱的。
不是标准格式,不是干净数据,而是各种口语化、碎片化记录。
第二,模型不能乱发挥。
工作内容必须真实,不能为了好看而编造。
第三,输出要符合具体场景。
同样是总结,周报、日报、复盘、项目汇报的写法都不一样。
第四,工具要有兜底。
模型不稳定、服务没启动、输入太短,都要考虑。
这几个点,比单纯调用一次模型更重要。
很多 AI 小项目看起来只是套了个 API,但真正做起来会发现,提示词设计、输入清洗、异常处理、结果展示,都会影响体验。
最后
这个周报生成器是一个很适合入门的 AI 应用项目。
它没有复杂算法,也没有一堆框架,但完整覆盖了一个小工具从输入到输出的全过程。
用户输入原始记录。
Python 清洗文本。
提示词组织任务。
本地大模型生成内容。
Streamlit 展示结果。
Markdown 文件下载保存。
这就是一个最小可用的 AI 应用。
如果你刚开始学 AI 应用开发,我建议不要一上来就做复杂 Agent。先做这种小工具,能解决一个真实问题,也能很快看到结果。