用大模型Function Calling打造AI日历助手:自然语言直接创建日程
不知道你有没有遇到过这样的情况明明只是想把“下周三下午三点和产品对一下排期”写进日历结果打开日历 App 后要新建事件、填标题、切日期、选时间、设提醒等折腾完原本三秒钟能说完的事已经花掉了半分钟。最开始我试图用系统自带日历的 Siri 捷径来解决后来又试过各种第三方日历的“语音建日程”但效果都不理想。不是识别不准就是格式死板必须按照固定句式说才能建成功。一旦句子稍微口语化比如“周五下班前提醒我把周报发了”就会要么识别失败要么建出一个时间、标题都不对的事件。后来我开始接触大模型应用开发意识到自然语言转日历事件这个需求其实非常适合用 LLM 的能力来解决。于是我就利用业余时间做了一个日历 AI 助手让它听懂人话然后自动在日历里建好日程。这篇文章会把我从需求拆解、技术选型、核心代码实现到部署和踩坑的完整过程整理出来希望给同样想做 AI 工具落地的小伙伴一些参考。本文适合有基础的 Python 开发经验同时对大模型 API 调用和 AI Agent 开发感兴趣的读者。如果你对“怎么让大模型输出稳定格式”“怎么把 AI 能力接到真实业务系统”这件事感兴趣这篇文章也很值得一看。1. 项目背景与整体方案设计1.1 手动敲日程到底麻烦在哪先列举一下手动建日程的几个痛点这也是我决定做这个工具的原因步骤太多。新建事件、填标题、选日期、选时间、选提醒至少五六步。不自然。人类说话是带口语和模糊时间的比如“下下周”“周五下班前”“后天上午”日历 App 并不天然理解这些说法。批量记录很痛苦。如果一次性收到五六个会议时间逐条创建特别容易出错。跨日历时更麻烦。工作日历、个人日历分开手动切换再复制内容体验割裂。这些问题本质上都指向同一个需求能不能用一句自然语言直接完成日程的创建1.2 AI 日历助手的产品定位我先把这个 AI 日历助手的边界划定清楚避免做成一个“什么都能干但什么都干不好”的项目。输入一句自然语言例如“下周三下午三点和产品对一下新版本的排期记得提前半小时提醒”。输出在指定的日历账号中创建一条结构化日程事件日历内容包含标题、开始时间、结束时间、地点、备注、提醒时间。辅助能力查询某个时间段已有日程方便做冲突检测。非目标不做完整 UI 界面不做复杂的多人协作不做日历订阅源管理。这个定位很重要。它意味着开发重点是“自然语言 → 结构化事件”的转换层和“结构化事件 → 日历写入”的执行层而不是去重新做一个日历。1.3 技术选型在技术选型时我对比了三套方案。第一套是基于规则和正则表达式解析。优点是便宜、快、可控缺点是只能支持固定句式改一个说法可能就解析失败维护成本高。第二套是直接让大模型输出 JSON 文本再在代码里用json.loads解析。优点是开发快缺点是大模型偶尔会在 JSON 前后加解释文字或者字段名漂移导致解析失败。第三套是使用 function calling 机制也叫工具调用。让大模型先理解用户意图然后输出一个结构化的“函数调用参数”程序端定义好函数签名大模型自动把自然语言里的关键信息映射到函数参数上。最终我选择了第三套方案。原因很简单function calling 会在模型层面约束输出结构比让模型自由生成 JSON 要稳定得多。参数有明确 schema字段不会乱漂移。后续扩展时只需要增加新工具函数AI 助手就能自动学会调用不需要改解析器。日历写入方面我用的是 CalDAV 协议。CalDAV 是日历同步的主流标准协议Google Calendar、Apple iCloud Calendar、Nextcloud Calendar、BAIKAL 等都支持。通过 CalDAV 写入意味着这个助手不绑定某一家日历服务只要支持 CalDAV 就能用。1.4 整体工作流程整个系统工作流程可以拆成下面几条链用户输入一句自然语言日程描述。系统把用户指令连同已定义的工具函数 schema 一起发给大模型。大模型判断用户意图输出“创建日程”或“查询日程”的调用参数。程序解析大模型返回的工具调用信息调用对应的本地函数。本地函数通过 CalDAV 协议操作日历数据返回执行结果。系统把执行结果再次交给大模型由大模型生成一段自然语言回复返还给用户。这里有一个关键点真正读写日历的是本地代码大模型只负责“听懂人话并变成参数”。这样既保证格式的可控性也保证安全边界清晰——大模型永远不会直接拿到你的日历账号密码。2. 环境准备与项目初始化2.1 环境版本说明本文示例以 Python 3.10 环境为基础操作系统不限我本地开发用的是 macOS部署到 Linux 服务器时也正常运行。版本需要根据你的项目实际情况调整本文演示代码重点说明配置思路不同小版本之间通常可以兼容。建议提前准备好以下基础环境Python 3.10 或更高版本pip 包管理工具一个可用的大模型 API兼容 OpenAI function calling 格式即可一个支持 CalDAV 的日历账号我用到的核心 Python 库包括库用途openai调用大模型 API使用 function calling 能力caldavPython 操作 CalDAV 协议python-dateutil处理相对时间、时区、重复规则pydantic定义事件模型做数据校验icalendar生成和解析 iCalendar 格式的日历事件python-dotenv读取 .env 配置文件中的密钥2.2 项目目录结构整个项目我采用按模块拆分的结构这样以后增加新工具或者换模型时不会牵一发动全身。calendar-ai-assistant/ ├── .env # 密钥和配置文件不入库 ├── requirements.txt # 项目依赖 ├── main.py # 主入口接收用户输入 ├── app/ │ ├── __init__.py │ ├── config.py # 配置读取 │ ├── models.py # 数据模型 │ ├── assistant.py # AI 助手核心逻辑 │ ├── tools/ │ │ ├── __init__.py │ │ ├── calendar_tools.py # 日历工具函数 │ │ └── caldav_client.py # CalDAV 客户端封装 │ └── parser.py # 自然语言解析与工具调用创建好目录后先把依赖写入requirements.txtopenai1.30.0 caldav1.3.6 python-dateutil2.9.0 pydantic2.7.0 icalendar5.0.0 python-dotenv1.0.0然后安装依赖pip install -r requirements.txt2.3 配置文件 .env在项目根目录创建.env文件保存模型 API 配置和日历账号配置# 模型 API 配置 LLM_API_KEYyour-api-key LLM_BASE_URLhttps://api.openai.com/v1 LLM_MODELgpt-4o-mini # CalDAV 日历配置 CALDAV_URLhttps://caldav.example.com CALDAV_USERNAMEyour-username CALDAV_PASSWORDyour-password CALENDAR_PATH/remote.php/dav/calendars/username/calendar-name/这里要特别提醒.env必须加入.gitignore避免密钥被提交到公开仓库。为了避免在公网传输密钥实际部署时建议把密钥放到环境变量或密钥管理服务中工具函数需要访问密钥时再从配置中心读取。2.4 配置读取模块创建app/config.pyimport os from dotenv import load_dotenv load_dotenv() class Config: LLM_API_KEY: str os.getenv(LLM_API_KEY, ) LLM_BASE_URL: str os.getenv(LLM_BASE_URL, https://api.openai.com/v1) LLM_MODEL: str os.getenv(LLM_MODEL, gpt-4o-mini) CALDAV_URL: str os.getenv(CALDAV_URL, ) CALDAV_USERNAME: str os.getenv(CALDAV_USERNAME, ) CALDAV_PASSWORD: str os.getenv(CALDAV_PASSWORD, ) CALENDAR_PATH: str os.getenv(CALENDAR_PATH, ) config Config()这个配置文件做的事情很简单集中管理所有环境变量后面任何模块要读取配置时都从Config获取而不是散落在各个文件中这样后期维护方便很多。3. 数据模型与核心设计3.1 定义日程事件模型为了让大模型输出的参数能够被安全校验我使用 Pydantic 定义一个清晰的日程事件模型。创建app/models.pyfrom typing import Optional, List from pydantic import BaseModel, Field class CalendarEvent(BaseModel): 统一日历事件模型 summary: str Field(description日程标题) description: Optional[str] Field(None, description日程备注描述) location: Optional[str] Field(None, description地点) start: str Field(description开始时间ISO8601格式) end: str Field(description结束时间ISO8601格式) remind_minutes: int Field(30, description提醒时间提前多少分钟) calendar_id: Optional[str] Field(None, description目标日历ID缺省使用默认日历) class CalendarQuery(BaseModel): 查询日程的参数模型 start: str Field(description查询开始时间ISO8601格式) end: str Field(description查询结束时间ISO8601格式)这里的时间统一使用 ISO8601 格式例如2025-03-18T15:00:0008:00。ISO8601 的好处是自带时区信息不会因为服务器时区和用户时区不同导致时间错乱。3.2 CalDAV 客户端封装创建app/tools/caldav_client.py先封装一个最小可用的 CalDAV 客户端。from datetime import datetime, timedelta from typing import List, Optional import caldav from icalendar import Calendar, Event as ICalEvent from app.config import config class CalDAVClient: CalDAV 日历客户端 def __init__(self): self.client caldav.DAVClient( urlconfig.CALDAV_URL, usernameconfig.CALDAV_USERNAME, passwordconfig.CALDAV_PASSWORD ) self.principal self.client.principal() self.calendar self._get_calendar() def _get_calendar(self): 获取指定日历如果没指定则取第一个日历 calendars self.principal.calendars() if not calendars: raise RuntimeError(该账号下没有找到可用的日历) if config.CALENDAR_PATH: for cal in calendars: if config.CALENDAR_PATH in cal.url: return cal return calendars[0] def create_event( self, summary: str, start: datetime, end: datetime, description: str , location: str ) - str: 创建一条日程返回事件UID event ICalEvent() event.add(summary, summary) event.add(dtstart, start) event.add(dtend, end) if description: event.add(description, description) if location: event.add(location, location) cal Calendar() cal.add_component(event) # 写入后重新读取拿到服务端分配的UID saved self.calendar.save_event(cal.to_ical().decode(utf-8)) return saved.id def query_events( self, start: datetime, end: datetime ) - List[dict]: 查询指定时间范围内的日程 events self.calendar.date_search(startstart, endend) result [] for event in events: data event.data cal Calendar.from_ical(data) for component in cal.walk(): if component.name ! VEVENT: continue result.append({ summary: str(component.get(summary, )), start: str(component.get(dtstart, )), end: str(component.get(dtend, )), }) return result这里有几个实现细节值得解释一下。第一create_event方法内部使用了icalendar库来构造符合 iCalendar 规范的事件对象然后转成字符串交给caldav库保存。因为 CalDAV 本质上传输的就是 iCalendar 格式的数据所以要自己补上 event 的必填字段。第二query_events中把日期的 ISO 格式直接转成了字符串返回。这个设计是为了后续把这些数据回传给大模型时可以用文本形式让模型理解。第三save_event返回的事件对象可以拿到服务端生成的 ID这个 ID 可以作为后续编辑、删除事件的依据。3.3 定义工具函数大模型的 function calling 需要一个明确的工具 schema。这里我把工具定义放在代码里方便复用创建app/tools/calendar_tools.pyfrom datetime import datetime from typing import List from app.models import CalendarEvent, CalendarQuery from app.tools.caldav_client import CalDAVClient class CalendarTools: 暴露给 LLM 使用的日历工具 def __init__(self): self.client CalDAVClient() def create_calendar_event(self, event: CalendarEvent) - dict: 创建日历事件 start datetime.fromisoformat(event.start) end datetime.fromisoformat(event.end) uid self.client.create_event( summaryevent.summary, startstart, endend, descriptionevent.description or , locationevent.location or ) return { success: True, event_uid: uid, summary: event.summary, start: event.start, end: event.end, } def query_calendar_events(self, query: CalendarQuery) - dict: 查询日历事件 start datetime.fromisoformat(query.start) end datetime.fromisoformat(query.end) events self.client.query_events(startstart, endend) return { success: True, count: len(events), events: events, } tools CalendarTools() # 这个字典会被转换成大模型的 function schema TOOL_SCHEMAS [ { type: function, function: { name: create_calendar_event, description: 在日历中创建一条日程事件。当用户表达需要安排会议、提醒、待办事件等意图时使用。, parameters: { type: object, properties: { summary: { type: string, description: 日程标题尽量简洁 }, description: { type: string, description: 日程的详细描述或备注 }, location: { type: string, description: 日程地点 }, start: { type: string, description: 开始时间ISO8601 格式 }, end: { type: string, description: 结束时间ISO8601 格式 }, remind_minutes: { type: integer, description: 提前提醒分钟数默认30分钟 } }, required: [summary, start, end] } } }, { type: function, function: { name: query_calendar_events, description: 查询日历中某段时间内已有的日程事件。当用户询问某天有没有空、查询日程安排时使用。, parameters: { type: object, properties: { start: { type: string, description: 查询开始时间ISO8601 格式 }, end: { type: string, description: 查询结束时间ISO8601 格式 } }, required: [start, end] } } } ]3.4 关键工具函数列表为了让后面代码看起来更清晰这里用表格把工具函数的作用整理一下。函数名用途触发场景示例create_calendar_event创建日程事件“周三下午四点跟李总开会”query_calendar_events查询已有日程“我周五有什么安排”在实际项目中你还可以扩展更多工具比如update_calendar_event修改已有日程。delete_calendar_event取消日程。add_todo添加到待办清单。扩展方式都是在TOOL_SCHEMAS中增加一个函数定义然后实现对应方法并在调用转发逻辑中加一个分支。4. 实现核心 AI 助手4.1 生成带时区的当前时间大模型并不知道当前的“今天”是什么日期尤其是用户输入“下周三”这种相对时间时必须给模型一个可参照的锚点。所以我需要在每次请求模型时把当前时间传给模型。创建一个app/parser.pyfrom datetime import datetime, timedelta import json from openai import OpenAI from app.config import config from app.tools.calendar_tools import tools, TOOL_SCHEMAS class CalendarAIAssistant: def __init__(self): self.client OpenAI( api_keyconfig.LLM_API_KEY, base_urlconfig.LLM_BASE_URL ) self.system_prompt self._build_system_prompt() staticmethod def _build_system_prompt() - str: 构造系统提示词 return ( 你是日历助手。你会收到用户关于日程安排的自然语言指令。 你的任务是把用户的指令解析为工具调用。 注意今天日期和当前时间会由用户消息提供所有相对时间都以它为准。 如果用户给出的时间不够明确需要根据上下文合理推断并在回复中说明推断结果。 如果没有可用日历不要编造日程而要告诉用户你无法完成操作。 ) def _get_current_time_text(self) - str: 获取带时区的当前时间文本 now datetime.now().astimezone() return now.isoformat() def chat(self, user_input: str) - str: messages [ {role: system, content: self.system_prompt}, { role: user, content: ( f当前时间{self._get_current_time_text()}\n f用户指令{user_input} ) } ] response self.client.chat.completions.create( modelconfig.LLM_MODEL, messagesmessages, toolsTOOL_SCHEMAS, tool_choiceauto, ) message response.choices[0].message # 如果模型没有调用工具直接返回文本 if not message.tool_calls: return message.content or 我暂时无法处理这个请求。 # 执行工具调用 for tool_call in message.tool_calls: function_name tool_call.function.name arguments json.loads(tool_call.function.arguments) if function_name create_calendar_event: result tools.create_calendar_event( # 这里用 Pydantic 对参数做一下反序列化 CalendarEvent(**arguments) ) else: result {success: False, error: f未知工具: {function_name}} messages.append(message) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) # 把工具执行结果交回给模型生成用户可读的回复 final_response self.client.chat.completions.create( modelconfig.LLM_MODEL, messagesmessages, ) return final_response.choices[0].message.content or 执行完成。创建main.pyfrom app.parser import CalendarAIAssistant def main(): assistant CalendarAIAssistant() print(日历 AI 助手已启动输入 exit 退出。) print(示例下周三下午三点和产品对一下排期提前半小时提醒我) while True: user_input input(\n ).strip() if user_input.lower() in (exit, quit): print(再见) break if not user_input: continue reply assistant.chat(user_input) print(\n日历助手, reply) if __name__ __main__: main()这里有一个 Python 导入方面的小坑会影响新手上手我上面把CalendarEvent用到了但是还没有导入它需要修改app/parser.py里的导入部分从app.models里补上CalendarEvent。实际代码在parser.py顶部加上这行from app.models import CalendarEvent4.2 为什么模型需要“当前时间”做过 AI Agent 开发的朋友应该都知道大模型的知识截止日期和系统当前时间未必一致。如果用户说“这周五下午有会议”模型离开了当前时间锚点根本无法确认“这周五”是哪一天。解决办法就是把当前时间作为上下文注入到用户消息中。这样模型就知道“今天是某年某月某日所以这周五是哪一天”。这是非常关键的工程细节如果漏掉这一步相对时间解析就会完全错乱。4.3 为什么使用 function calling 而不是让模型直接输出 JSON有些开发者会更倾向于让模型直接输出 JSON省去 tools 相关的配置。但我在实际开发中明显感觉到function calling 的稳定性和约束性比自由文本输出 JSON 高很多主要体现在四个方面。第一function calling 是模型厂商针对工具调用场景做过专项训练的模型内部会以更严格的格式输出调用参数很少出现多余的解释文字。第二在自定义的 tools schema 中我们可以为每个字段写清楚含义和枚举值这相当于在提示词之外增加了一层结构化约束。第三如果采用“让模型直接输出 JSON”的方案时间字段大概率会出现各种格式比如2025年3月18日下午3点、2025-03-18 3pm。function calling 配合清晰的字段描述会让模型更倾向于输出统一的 ISO8601。第四后续想增加新能力时function calling 天然支持多工具分发。可以让模型自己在多个函数之间选择调用而自由输出 JSON 则需要自己写一套意图分类逻辑。4.4 处理多轮对话中的工具返回上面的代码里还有一个容易被忽略的点当模型调用工具之后我们把message包含工具调用信息的 assistant 消息追加到messages然后把工具执行结果以roletool的消息追加回去。这是 OpenAI 对话 API 对工具调用的硬性要求必须携带完整的、带有tool_call_id的 assistant 消息再把工具结果对应上去。如果把这条 assistant 消息省略了或者漏掉tool_call_id第二次请求会直接报错。对于一次对话中模型连续调用多个工具的情况循环遍历message.tool_calls每个工具调用都会有一个独立的tool_call_id把他们分别对应到工具结果即可。5. 运行验证与效果演示5.1 启动助手在项目根目录运行python main.py如果一切配置正常你会看到日历 AI 助手已启动输入 exit 退出。 示例下周三下午三点和产品对一下排期提前半小时提醒我 5.2 示例输入与预期输出先来测试一下基本的建日程功能 下周三下午三点和产品对一下新版本排期地点在3楼会议室记得提前半小时提醒我预期 AI 助手返回类似下面的回复好的我已帮你创建日程 标题和产品对一下新版本排期 时间2025-03-19 15:00:00 至 2025-03-19 16:00:00 地点3楼会议室 提醒提前30分钟如果日历账号里已经配置好了 CalDAV打开你自己的日历客户端就能看到这条日程已经被同步过来。接下来测试一下查询已有日程 我这个周五下午有空吗助手会调用query_calendar_events工具然后结合库中返回的事件数据用自然语言回答周五下午你目前有2条日程14:00-15:00 跟设计评审16:30-17:00 周报提交提醒。5.3 真实同步验证为了验证日历确实被写入成功可以直接在 Python 中调用底层模块做快速验证from datetime import datetime from app.tools.calendar_tools import tools result tools.create_calendar_event( event{ summary: 测试同步, start: 2025-03-20T10:00:0008:00, end: 2025-03-20T10:30:0008:00, } ) print(result)如果你看到类似下面的输出说明 CalDAV 同步链路是通的{success: True, event_uid: xxx-xxx-xxx, summary: 测试同步, start: 2025-03-20T10:00:0008:00, end: 2025-03-20T10:30:0008:00}6. 已遇到的高频问题与排查思路开发过程中踩了不少坑这里挑几个典型问题按现象、原因、解决思路整理成表格方便大家对照排查。问题现象常见原因解决思路时间总是提前/延后8小时忽略了时区datetime 对象不带 tzinfo创建日程时使用带时区的 ISO8601 时间或调用astimezone()转成本地时间模型返回的 JSON 解析失败模型自由输出内容带了额外的说明文字改用 function calling严格按工具参数输出工具调用后第二次请求报错“Invalid tool_call_id”没有把带工具调用的 assistant 消息追加回上下文检查 messages 列表是否保留了 assistant 消息以及 tool 消息数量一致CalDAV 创建成功但客户端看不到日历路径选错写到了别的日历确认CALENDAR_PATH配置的路径是否指向目标日历创建日程时提示“DTSTART 必须带 VEVENT”构造 iCalendar 事件时缺少必填字段用icalendar库构造事件别直接手写字符串拼 iCalAPI 返回 401API Key 无效或没有对应模型权限检查.env中的密钥、base_url、model 名称是否正确中文标题乱码iCalendar 编码处理不对尽量用icalendar库并确保保存时使用 utf-8 字符串用户事件总是被重复创建程序在创建前缺少去重逻辑在调用模型前先查询同一时间段若已存在相同标题的日程先与用户确认相对时间解析不准模型不知道当前日期每次请求时在 user 消息中写入当前时间点消息长度超限多轮对话累积了过多历史为历史消息设置最大轮数如保留最近10条超长摘要后截断网络请求超时大模型 API 或 CalDAV 服务响应慢设置合理的 timeout增加失败重试和指数退避除了表格中的问题还有一个很容易被忽视的点用户说话的目的未必是建日程。比如用户说“如果我周五没空怎么办”模型可能误判成创建日程。对于这种边界情况需要在系统提示词中明确规定当用户只是在提问或表达假设时不要调用创建工具优先查询已有日程并分析判断。如果用户在一个句子里同时表达了多个日程比如“周三下午开会周五上午和客户吃饭”模型会尝试调用多次create_calendar_event。这时要注意代码中的循环目前我的代码已经支持遍历多个tool_calls但需要确认每次调用后的事件结果都能被正确地回传给模型并且在最终输出中逐条给用户反馈。7. 工程化实践与扩展建议7.1 代码层面的优化点核心 Demo 跑通之后如果要真正作为个人工具长期使用以下几件事值得认真对待。第一异常处理。目前工具直接暴露给上层一旦 CalDAV 服务暂时不可用整个对话会抛异常。更合理的做法是给每个工具调用包一层 try-except返回一个结构化的错误信息例如try: result tools.create_calendar_event(...) except Exception as e: result {success: False, error: f创建日程失败: {e}}第二日志记录。要记录模型 request、response 摘要、工具名和工具执行耗时。日志不仅能帮助排查问题还能让你了解模型在某些输入下是否频繁误判工具从而调整提示词。第三参数校验。从大模型返回的参数必须经过 Pydantic 校验拒绝不合法数据。比如end时间一定要晚于start时间否则要跟模型反馈错误让模型修正。第四幂等设计。设计要尽量避免重复创建日程。最简单的方式是如果用户指令涉及创建先查询同一时间窗口内是否已经有标题类似的日程。如果有先向用户确认。不要让自己创建的日程因为重复调用而出现多条。7.2 配置与密钥管理代码里的.env只是本地开发的便利方案。真正用在生产环境或云服务器时密钥需要放环境变量。我建议至少遵循以下原则不要把敏感信息写进代码仓库。给 API Key 和日历账号设置最小权限例如只读或只写指定日历的专用账号。如果模型 API 和 CalDAV 服务不在同一内网尽量通过 HTTPS 访问避免密钥在网络传输中泄漏。不要把整份对话日志原样存到第三方日志服务里面可能包含个人信息和会议细节。7.3 多日历场景如果用户同时使用家庭日历和工作日历那么在用户指令中就要允许指定日历来源。我目前在模型中增加了一个可选的calendar_id字段来做区分。为了让模型能识别哪个日历更匹配需要在工具描述里写明每个日历的用途。例如calendar_work工作日历 calendar_private私人日历输入“周三下午和客户开会”时模型倾向于写入calendar_work输入“周六陪娃去公园”时就会写入calendar_private。7.4 从交互式命令行到应用服务目前这个版本以命令行交互为主方便验证和演示。对于一个更接近真实产品的形态建议抽象出一层“助手服务”把它封装成 HTTP 接口通过 FastAPI 暴露/chat接口接收 JSON{message: ...}。服务内部根据user_id维护各自的配置和对话上下文。支持接入 IM 机器人比如钉钉、飞书、企业微信、Telegram 机器人。增加多用户隔离避免用户A创建的日程写到用户B的日历里。这也正是这类 Calendar AI Assistant 从个人玩具变成团队工具时会遇到的典型问题。7.5 更智能的冲突检测与摘要演示版本已经具备了创建日程和查询日程两个基础能力。在此基础上还能继续加三个实用的进阶工具。第一个是check_conflicts工具可以接收多个时间区间然后返回哪些区间与已有日程冲突。这个工具在创建会议时尤其有用比如可以说“周五下午一小时时间别安排在我组会期间”。第二个是generate_meeting_summary工具输入一段会议记录的文字或语音转写文本让模型提取会议讨论中的 action item直接为每个人创建对应的日程和待办。第三个是记忆能力让助手记住一些长期偏好比如“我所有日程都默认提前15分钟提醒”“每周一上午不开会”。这个能力可以靠额外的偏好存储模块实现不一定需要引入完整的外置记忆数据库。多写几个文本或 JSON 都可以承载。总结做这个日历 AI 助手的核心收获其实不是“日历同步”本身而是把大模型从“对话机器”变成“工具使用者”的一整套工程思路。整套程序只有 300 行左右代码核心就是三个模块用 function calling 把自然语言变成结构化参数用 Pydantic 校验数据结构用 CalDAV 同步到真正被大众使用的日历服务中。如果你也想做一个能解决真实痛点的 AI 小工具这套架构完全可以复用到其他场景比如 AI 记账助手、AI 待办助手、AI 邮件助手。最关键的一点别让 AI 直接操作真实的业务系统而是让 AI 学会“说结构化的话”由代码去完成最终的执行动作。这样一个简单的分层就足以让工具可控、可排错、可维护。如果你最近也被手动敲日程折磨过可以试试这个方案把入口换成一句人话剩下的事情交给代码和日历系统去完成。