做日程管理最烦的不是事件本身,而是反复填空。打开日历应用、点新建、选日期、选开始时间、填标题、填地点、再填提醒,一个跨部门会议往往要填五分钟。更麻烦的是日程来源五花八门:微信里一句“明天下午三点和客户对方案”、邮件里一条“周四上午十点周会”、Excel 里一整列培训安排,每一处都要手工拆解,再一条一条敲进日历。
这次分享我开发的一款日历 AI 助手。它解决的问题很具体:把一段自然语言直接变成一条结构化日程。比如你输入“下周三下午 3 点与产品组开需求评审会,需要会议室 A,时长 1 小时”,系统自动解析出标题、日期、开始时间、结束时间、地点、参与人,生成日程卡片,确认后写入日历。它也能处理“每周五上午十点提交周报”这类重复日程,或者一条文本里包含多个会议的情况。
这篇文章会把开发这款日历 AI 助手时的整体设计、核心实现、关键模块、接口能力和测试思路写清楚。内容既覆盖“自然语言到结构化事件”这条主线,也会讨论日期解析的坑、冲突检测怎么做、批量导入怎么设计,以及本地部署和接口接入的注意事项。如果你正打算自己写一个类似工具,或者被手动维护日程折磨到想自动化,这篇可以直接收藏。
1. 日历 AI 助手核心能力速览
先给一张总览表,把整体能力说明白。
| 能力项 | 说明 |
|---|---|
| 项目定位 | 基于自然语言输入生成结构化日历日程的个人效率工具 |
| 核心功能 | 自然语言日程创建、重复日程识别、多事件拆分、冲突检测、批量导入、提醒设置 |
| 输入方式 | 文本输入 / CSV 批量导入 / API 调用 |
| 输出方式 | JSON 结构化日程数据、Web 日历视图、接口返回 |
| 技术路线 | 大模型调用 + 规则解析兜底 + 日程存储服务 |
| 推荐运行环境 | Linux / Windows / macOS 均可,模型层可选云端 API 或本地开源模型 |
| 显存要求 | 规则库 + 云端 API 模式无显存要求;本地模型模式取决于模型尺寸,需实测 |
| API 支持 | 支持 HTTP 接口创建日程和查询日程 |
| 批量任务 | 支持 CSV 批量导入和批量日程解析 |
| 主要调用方式 | Web 界面、命令行脚本、REST API |
| 适合场景 | 个人日程整理、团队会议纪要拆解、邮件/微信日程信息快速落库 |
这里说明一下:日历 AI 助手本质上是“大模型语义理解 + 规则化时间处理 + 数据库”三者的结合。它并不是要求你必须本地跑一个 70B 大模型,云端接口也能完成大部分任务。开发时可以根据隐私要求灵活切换解析层。
2. 适用场景与使用边界
日历 AI 助手适合以下几类用户:
第一类是每天日程零碎但量大的人。产品经理、项目助理、销售、医疗和教育行业的排班人员,每天都要面对大量文字形式日程。手工录入浪费时间,自动化解析后只需人工确认,效率提升明显。
第二类是需要统一多个来源日程的人。企业微信里的会议、邮件里的邀约、Excel 里的培训计划,不同来源格式不同。设计时可以把它们统一成 CSV 或文本格式,再交给助手批量生成标准日程数据。
第三类是开发者。日历 AI 助手提供了 API,可以接到自己的 OA、项目管理系统或自动化工作流里。比如每周自动拉取邮件,让模型提取会议时间,直接写入公司日历。
不过,它也有明确的使用边界:
- 不适合处理模糊到无法判断的时间表达。比如“改天开会”“有时间约一下”,模型无法可靠地推断具体时间,此时应该主动要求补充时间,而不是猜一个时间写进日历。
- 不适合在隐私要求极高的场景直接调用云端模型。会议主题、参与者、会议地点往往涉及公司内部信息,如果放到外部大模型 API,存在数据泄露风险。建议使用本地模型,或者对输入文本做脱敏预处理。
- 不适合替代人工最终确认。自动写日历前必须有一层确认机制,尤其是多人会议、会议室预约场景,错误日程比没有日程更麻烦。
还有一个合规点要强调:AI 解析的结果不能作为唯一事实来源,最终写入前要经过用户确认。所有涉及他人时间、部门会议、外部客户会议的内容,建议只做辅助建议,不要静默覆盖原日历。该项目只处理“把文字变成可管理日程”这一层,不替用户决策是否参会。
3. 日历 AI 助手整体架构设计
开发日历 AI 助手时,我把它分成四个模块:输入解析层、时间与事件处理层、存储层、展示与接口层。
输入解析层负责接收用户输入的一段自然语言或批量文本。这里要处理的典型情况是:一句话里包含多个事件、包含相对时间(“后天”)、包含自然表达(“下午茶”“下班前”)。单纯用正则根本无法覆盖这些语义,所以匹配一个轻量级大模型来做意图识别和信息抽取。
时间与事件处理层非常关键。大模型输出的是文本答案,要把它转成“事件”就需要规范化时间、判断事件类型、处理重复规则。
具体来说,这一层要解决三个问题:
- 时间归一化。把“下周三”“明天上午”“16:30”统一成标准 ISO 时间。
- 事件拆分。一条输入“周一开例会,周二和客户吃饭,周五提交周报”需要正确拆成三个事件,而不是合并成一个。
- 重复日程解析。识别“每周”“每天”“每两周”“工作日”等描述,生成 RRULE 规则。
存储层方面,项目可以采用 SQLite(轻量单机场景)或 PostgreSQL(多人、多端同步场景)。日程表设计中除了存标题、开始时间、结束时间、地点,还需要存原始文本、解析状态、确认状态。
展示与接口层采用 Web 页面。用户可以粘贴文本、预览解析出的日程卡片、点击确认后写入日历。同时提供 REST API,方便其他脚本调用。
整个调用过程是这样:
用户输入文本 → 解析层判断是否需要调用大模型 → 模型输出中间 JSON → 规则层校验并修正时间 → 生成标准事件结构 → 用户确认 → 写入存储层 → 日历界面或 API 返回结果。
这里有个设计要点:不要把大模型输出直接当成最终数据写入数据库。大模型偶尔会把日期计算错,比如“下周三”在当前日期是周五时会识别错误。所以模型输出之后必须接一个规则层做二次校验。
以这个项目为例,一次完整调用链是:
用户粘贴文本:“下周三下午三点和产品组开评审会,会议室A,一小时” ↓ 模型结构化抽取 ↓ 输出: { "title": "产品需求评审会", "attendees": ["产品组"], "location": "会议室A", "start": "2025-07-16T15:00:00", "end": "2025-07-16T16:00:00" } ↓ 规则层校验“下周三”是否等于 2025-07-16 ↓ 确认页展示,用户点击确认 ↓ 写入日历4. 自然语言到结构化日程:模型调用策略
日程 AI 助手的核心骨架在“自然语言 → 结构化 JSON”这一步。这里没有采用训练专属小模型的方式,因为数据量不够,训练成本也不划算。更务实的方案是设计一套高质量的提示词模板,让通用大模型完成抽取。
给模型看的提示词模板大致这样设计:
你是一个日程信息抽取助手。请从用户输入的文本中抽取日程事件,并输出 JSON 数组。 要求: 1. 每个事件必须包含 title、start、end、location、attendees、is_recurring、rrule、reminder_offset_minutes 字段。 2. 如果没有明确结束时间,按开始时间后 60 分钟计算。 3. 如果一句话包含多个日程,必须全部抽取,不能合并。 4. 相对时间需要根据今天的日期 today 推算成具体的 ISO 8601 时间。 5. 无法推断具体日期时,title 正常输出,start 输出 null。 6. 重复日程使用 RFC 5545 RRULE 格式描述,例如每周一次为 FREQ=WEEKLY;INTERVAL=1。 今天是 {today} 用户输入:{user_input} 请只输出 JSON,不要输出解释。提示词里显式传入today很重要。没有这个基准日期,模型就无法可靠处理“下周三”“明天”“本月最后一个工作日”这类相对时间表达。开发时你会发现,同样的文本在今天和三天后输入,解析结果可能不同,因为相对基准变了。
实际开发中建议让模型输出一个 JSON,然后用代码做 JSON 解析和校验。很多大模型偶尔会在 JSON 前后加一段说明文字,所以解析和校验代码必须容忍 Markdown 代码块和多余文本。
代码示例:
import json import re def parse_model_output(llm_output: str): # 处理模型偶尔输出 markdown 代码块情况 text = llm_output.strip() fence_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text) if fence_match: text = fence_match.group(1).strip() # 直接找第一个 [ 到最后一个 ] start = text.find("[") end = text.rfind("]") if start == -1 or end == -1: raise ValueError("模型输出没有包含 JSON 数组") json_str = text[start: end + 1] events = json.loads(json_str) return events然后对返回的每个事件做字段级校验。一个很常见的坑是模型把开始时间算错。比如用户说“今天下午 4 点”,模型可能是对的;如果用户说“这周五下午 4 点”,而今天正好是周五,模型可能推到下周五。这是评估模型 prompt 时必须反复验证的点。
5. 日期时间解析:规则层兜底设计
光靠大模型并不够。日程类应用中时间准确性要求非常高,误差一天都可能带来实际损失。所以在模型输出后面加一个规则层,专门负责时间表达校验和修正。
规则层需要处理下面几类表达:
| 类型 | 示例 | 处理策略 |
|---|---|---|
| 绝对时间 | 2025-07-16 15:00 | 直接解析为标准时间 |
| 12 小时制 | 明天下午 3 点 | 结合 today + 偏移量计算 |
| 节假日或工作日 | 下周一 / 工作日 9 点 | 工作日需要跳过周末逻辑 |
| 时间段 | 15:00-16:00 | 分别解析 start 和 end |
| 相对周 | 下下周四 | 加 14 天计算,注意周起始日 |
| 循环规则 | 每周五上午 | 生成 RRULE 表达式,但首次 start 仍要确定 |
规则层最关键的是解决“单一时间基准”问题。在同一个请求内,today 必须固定,不能不同函数取到不同系统时间。下面给一个最基础的规则解析脚本示例,实际项目可根据需求扩展:
from datetime import datetime, timedelta import re def normalize_time_expression(text: str, today: datetime): """ 简易时间归一化示例:仅覆盖最基础的中文相对时间表达 """ if not text: return None text = text.strip() # 绝对时间示例,真实需求需要更完整解析 iso_match = re.match(r"(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})", text) if iso_match: return datetime( int(iso_match.group(1)), int(iso_match.group(2)), int(iso_match.group(3)), int(iso_match.group(4)), int(iso_match.group(5)), ) # “明天下午3点” if "明天" in text: day_delta = 1 text = text.replace("明天", "") elif "今天" in text: day_delta = 0 text = text.replace("今天", "") else: day_delta = 0 hour_match = re.search(r"(\d{1,2})点", text) if not hour_match: # 没有小时信息,默认早上 9 点 hour = 9 minute = 0 else: hour = int(hour_match.group(1)) minute = 0 minute_match = re.search(r"(\d{1,2})分", text) if minute_match: minute = int(minute_match.group(1)) result = today + timedelta(days=day_delta) return result.replace(hour=hour, minute=minute, second=0, microsecond=0)这个脚本只覆盖了一个非常小的范围,但说明了规则层的基本思路。成熟的方案需要结合 dateparser、python-dateutil 或自定义词典,把“中午”“凌晨”“下班前”“整点”“半点”这些中文习惯一起覆盖。
提醒一点:这里的规则层不是用来替代模型的,而是用来做校验和少数必须精确处理的场景。因为模型对中文口语的理解能力远超正则,但模型的日期计算容易出边界错误。两者结合,模型负责抽取语义,规则层负责纠正可验证的时间。
6. 日程存储设计与冲突检测
日历助手的数据模型不建议只存一张 event 表完事。至少要包含两个状态字段:parse_status(解析中 / 待确认 / 已确认)和 source_text(原始输入)。
保留 source_text 非常关键。用户确认日程后,如果发现解析错了,可以对照原始文本排查。而且后台可以积累一批用户纠正过的数据,之后用来迭代提示词模板。
推荐的日程表核心设计如下:
CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, location TEXT, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, all_day BOOLEAN DEFAULT 0, rrule TEXT, attendees TEXT, source_text TEXT, status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_events_start_time ON events(start_time); CREATE INDEX idx_events_status ON events(status);字段说明:
rrule存储循环规则。没有循环规则时为空,有循环规则时存标准 RRULE 字符串。status有 pending 和 confirmed 两种。pending 表示解析出来但尚未得到用户确认,不会出现在正式日历视图。attendees用 JSON 字符串存储参与人列表,简单场景下这样足够。
冲突检测策略上,这里采用“重叠事件”检测而不是“时间单位冲突”。比如一个会议是 15:00-16:00,另一个日程是 15:30-16:30,查询时直接判断两段时间区间是否重叠:
SELECT * FROM events WHERE status = 'confirmed' AND start_time < :end_time AND end_time > :start_time;查出重叠事件后,前端会把冲突事件用红色标识出来,提示用户是否继续。这里不要替用户做删除或合并决定,只提醒即可。
7. 接口 API 与批量导入设计
日历 AI 助手对外提供 HTTP API,方便通过脚本批量导入、或接入自己的系统。
最常用的两个接口是“自然语言创建日程”和“批量 CSV 导入”。
7.1 文本创建日程接口
设计成这样一个 POST 请求:
POST /api/events/parse Content-Type: application/json { "text": "下周一上午十点和小张开周会,地点在 3F 会议室,时长 1 小时", "timezone": "Asia/Shanghai", "today": "2025-07-11" }后端调用模型解析,然后将结果返回给前端确认。接口返回的 JSON 中 status 默认是 pending,前端展示确认按钮。
Python 后端接口可以用 FastAPI 实现,伪代码大致如下:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ParseRequest(BaseModel): text: str timezone: str = "Asia/Shanghai" today: str | None = None class ParseResponse(BaseModel): events: list parse_status: str source_text: str @app.post("/api/events/parse", response_model=ParseResponse) async def parse_events(req: ParseRequest): # 1. 调用模型解析文本 # 2. 规则层校验时间 # 3. 暂存为 pending,不覆盖真实日历 return ParseResponse( events=[], parse_status="pending", source_text=req.text, )这个接口只是示意,真正使用时事件列表要替换成模型实际解析结果。要注意设计接口时要有一个参数排除“静默覆盖”,任何创建/更新操作都必须是可确认的。
7.2 CSV 批量导入
批量导入场景通常用于一次性处理历史遗留日程。CSV 格式建议这样设计:
| 时间 | 时长 | 标题 | 地点 | 参与人 | 备注 |
|---|---|---|---|---|---|
| 2025-07-14 10:00 | 60 | 产品周会 | 3F 会议室 | 产品组 | 投影仪 |
| 2025-07-15 14:30 | 30 | 面试候选人 | 电话会议 | HR | 第二轮 |
批量导入的核心流程是先预览、后确认。上传 CSV 后系统解析成一批事件,前端展示一个事件列表,用户逐条核对或整体勾选,再点击“一键导入”。不能导入后直接覆盖日历。
接口设计可以是:
import csv def read_csv_events(file_path: str): events = [] with open(file_path, "r", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: event = { "title": row["标题"], "start": parse_csv_time(row["时间"]), "end": add_minutes(parse_csv_time(row["时间"]), int(row["时长"])), "location": row.get("地点", ""), "attendees": row.get("参与人", ""), } events.append(event) return events批量导入最容易踩的坑是编码问题。CSV 文件在 Windows 下通常是 GBK 编码,建议读取时统一用utf-8-sig或自动检测编码,然后在界面上给出错误反馈,避免用户看到乱码。
8. 功能测试与效果验证
日历 AI 助手的验证重点不在于界面,而在于解析准确率。开发时要准备一组覆盖不同表达方式的测试用例,每次调整提示词或规则后回归一次。
下面是基础的功能测试用例集:
| 编号 | 输入文本 | 期望结果 | 判定标准 |
|---|---|---|---|
| 1 | 明天下午3点开会 | title=会议,start=明天15:00 | 会议能被写入 |
| 2 | 每周五上午十点提交周报 | start=本周五10:00,rrule=FREQ=WEEKLY | 周报能生成重复日程 |
| 3 | 下周一和周二都有客户拜访 | 两个独立事件 | 一拆为二 |
| 4 | 7月20日全天出差 | all_day=true | 全天日程 |
| 5 | 今天下午开个会吧 | start=null | 未明确时间需要提示 |
| 6 | 下午4点会议室A面试 | location=会议室A,start=今天16:00 | 地点提取正确 |
| 7 | 培训安排在2025年12月1日 | start=2025-12-01 09:00 | 较远日期也能识别 |
| 8 | 周三下午3点产品会和4点设计会 | 两个事件且时间不重叠 | 同一句多事件切分正确 |
测试时还要重点验证“今日”“本周”边界。比如周四测试“本周日开会”和“这周末开会”时,模型容易理解错。需要把这类用例整理成测试矩阵,自动跑提示词 + 解析,而不是每次手动看结果。
效果验证的判断标准如表所示,不只是“看起来差不多”,而必须能落到开始时间、结束时间、地点、参与人、重复规则五个维度上。只有这五项全部正确,才算一条解析成功。
开发中建议在代码库里写一个自动回归脚本,便于后续替换模型或调整提示词时对比准确率。
9. 部署运行与资源占用观察
日历 AI 助手的部署方式取决于选择的模型层。
如果使用云端大模型 API,比如通过通用模型接口完成抽取,部署成本主要集中在后端服务和数据库上。一个轻量后端进程 + SQLite 足以支持个人使用,内存占用通常在几百 MB 级别。这样的模式不需要 GPU,不要求显存,适合放在普通服务器或家用 NAS 上运行。
如果希望在本地完成全部处理,比如使用开源模型并完全避免外部请求,就要关注显存占用。以目前常见的 7B~14B 级别中文对话模型为例,推理时的显存占用通常需要数 GB 到十几 GB 不等。这个数字受模型量化方式影响明显,实际运行前需要结合自己的显卡跑一遍。输入“安排一个会议”这类短文本并不像生成大段文章那样消耗算力,但模型加载本身还是会占用固定空间。
从资源观察角度看,建议这样分层:
- 模型服务单独启动,和后端逻辑服务分开,避免互相影响。
- 观察模型服务的显存占用用
nvidia-smi,观察内存占用用top或系统监视器。 - 测试不同并发数量,比如同时提交 5 条日程解析请求时,观察接口响应时间。如果单条解析达到 20 秒,说明模型推理耗时明显偏高,要考虑换小模型或上 GPU。
本地模型模式下,建议先把短文本解析的并发限制为 1。日程解析不像对话流那样对低延迟有极高要求,用户等 3~5 秒可以接受。因此优先保证质量和稳定,而不是盲目追求并发。
后端服务在开发阶段通常直接用开发服务器跑。部署到正式环境时,要避免开发服务器直接承担外部请求,建议套一层反向代理,并给 API 加访问限制范围。如果只给自己用,监听 127.0.0.1 就够了;如果需要给多人用,则需要加鉴权,避免任何人直接调用接口批量写入日程。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型输出不是合法 JSON | 提示词没有约束格式或模型幻觉 | 打印模型原始输出 | 增加解析容错,提取 JSON 片段后重试 |
| 日程时间算错一天 | 模型没拿到基准日期 today,或用了服务器 UTC 时间 | 检查调用参数中 today 和时区 | 后端固定基准时间并传 timezone |
| 一句话里多个日程被合并 | 提示词中没有强调多事件拆分 | 查看解析 JSON 数组长度 | 在提示词中增加“必须输出JSON数组”说明 |
| CSV 批量导入中文乱码 | CSV 文件编码不兼容 | 检查文件编码 | 统一转 utf-8-sig 后读取 |
| 接口能调用但写入失败 | 数据库连不上或表结构缺失 | 查看后端日志 | 初始化数据库表并检查连接配置 |
| 确认页日历没有刷新 | 前端没有重新拉取事件列表 | 打开浏览器调试窗口看请求 | 确认后调用 list 接口重新加载 |
| 本地模型推理慢 | 显存不足导致部分层在内存计算 | 观察任务管理器 GPU 占用 | 使用量化模型或换更大显存设备 |
| 同一条文本重复导入生成重复日程 | 没有做去重约束 | 查询数据库重复记录 | 增加 source_text + start_time 唯一约束 |
补充一个比较隐蔽的坑:如果后端服务跑在 Docker 容器里而容器时区是 UTC,生成的事件时间会整体偏移 8 小时。所以任何时候都要在应用内显式处理时区,而不是依赖运行环境默认时区。
11. 最佳实践与合规使用建议
做日历 AI 助手这类工具,工程上最值得记住的经验是:先保证正确,再追求效率。
记住下面这条最佳实践:
先把单条日程解析做到 95% 准确,再考虑批量导入和 API。日历数据错误带来的代价比手动录入大得多。开会地点错了、把下午 3 点识别成下午 13 点,都会让用户失去对工具的信任。
开发阶段建议这样做:
- 维护一套 golden test set,也就是标注好正确答案的测试集。每次修改提示词必须跑一遍,逐步沉淀出稳定的测试集。
- 对可疑输出使用低置信度标记。如果模型给出的时间字段为空,或者规则层发现时间明显不合理,界面要显示黄色警告,不能静默通过。
- 所有日程写入前都走确认流程。即使以后扩展到 API 自动写入,也应该有一个“先到待确认队列,再由定时任务写入”的模式。
- 数据做好备份。日历助手会积累大量个人时间数据,数据库备份策略不能省。
使用合规方面要注意:
- 不要直接上传包含完整人名、手机号、详细业务机密的原始文本到未知的云端服务。建议配置允许用户选择自定义 API 地址或本地模型。
- 不要在项目演示数据中包含他人真实会议信息、真实客户姓名,测试时用虚构人物和虚构会议代替。
- 如果将日程数据导出或参与模型训练,需要获得用户明确同意并做去标识化处理。
- 涉及企业内部会议时,AI 生成结果只能视为辅助归档,不能替代消息通知,也不应把他人日程信息公开暴露给无关人员。
12. 总结与下一步
这次日历 AI 助手从立项到可用,核心价值围绕一个点:把“非结构化文本日程”转换成“结构化日历事件”。它不是复杂的 AI 算法项目,而是一个把大模型能力、规则解析、存储设计结合得比较完整的工具型应用。
如果你准备复刻或改造,建议按这个顺序推进:第一步,先手动调用大模型接口,测试它在你的真实输入样本上的解析准确率;第二步,补齐规则层,重点处理时间和多事件拆分;第三步,实现 Web 确认界面;第四步,再考虑批量 CSV 导入和 API 开放。最容易劝退的坑出现在第一步和第二步之间,也就是模型输出能看但字段一旦细究就会出错。这时候不要急着堆 Prompt 技巧,先把“期望输入输出样例”细化到开始、结束、参与人、重复规则四要素,再针对性迭代。
下一步值得扩展的方向有:通过订阅日历服务把确认后的日程直接同步到手机日历;增加语义冲突提醒,比如模型识别出两个会议靠得太近时给出休息建议;也可以把解析层做成可插拔服务,支持用户切换不同大模型后端。只要数据隐私边界清楚、确认流程完整,这类日历 AI 助手完全能从一个个人脚本长成接口化的小型生产力工具。