AI 工具

Kimi 会议纪要提取:JSON Schema 待办与日历文件

粘贴一份会议纪要,用 responseformat 的 jsonschema(strict) 拿到待办清单:谁、做什么、截止日期。程序核对负责人确实在纪要里出现过、日期合法,原文里写明的日期由程序补全。最后导出成 .ics 文件,可以直接导入手机或电脑日历。

BINBHO · 图文教程

粘贴一份会议纪要,用 response_format 的 json_schema(strict) 拿到待办清单:谁、做什么、截止日期。程序核对负责人确实在纪要里出现过、日期合法,原文里写明的日期由程序补全。最后导出成 .ics 文件,可以直接导入手机或电脑日历。

先看清成品

  • 测试: pytest 4 项通过。
  • 本地 qwen2.5 1.5B 实测: 3 条待办都提取出来了,其中 2 条有截止日期(10-28 和 10-24,由程序从原文补全),刘洋那条标为“日期待定”。导出了 2 个日历事件。
  • qwen3 0.6B: 漏掉了王敏的待办。

没有在 box 上跑的部分: box 上没有 Moonshot API Key,没有调用过 Kimi 官方 API。

  • 用到的替身: 文中的 KIMI_BASE_URL 指向两种替身。
    • 本地 Ollama 的 OpenAI 兼容接口。
    • A 篇自己写的 fake_moonshot.py。它是我按官方文档写的模拟服务器,只模拟了文件上传、抽取、删除这几个接口:用 pdftotext 抽取文字,对话转发给本地 Ollama。
  • 结论: 下面的成功和失败都是本地小模型的表现,不代表 Kimi 本身的效果。

合规提醒:Kimi API Key 只放在环境变量里。上传的文件会存到 Kimi 开放平台的账号下,含有个人信息、合同、病历等敏感内容的文件,要先确认公司的规定和对方是否同意。用完的文件及时删除(本文的代码会自动删除)。模型的回答只能作为参考,涉及报销、人事、法律的结论以原文件和负责人的确认为准。价格以官网“定价”页为准,本文不列价格。

做完之后你会得到什么?

成果 说明
meeting_todo.py 定义 schema → 获取 JSON → 核对负责人 → 补全原文里写明的日期 → 导出 .ics
meeting.txt 虚构的周会纪要
test_todo.py 负责人核对、日期补全(日期写在 due 或 task 里都能识别)、生成 .ics 格式

需要准备哪些环境?

项目 要求 说明
Python 3.9 以上 pip install openai pytest
模型 Kimi API,或本地 Ollama 模型 Kimi 文档的 response_format 页介绍了 json_object 和 json_schema 两种方式

文件结构

42-kimi-C-src/
├── meeting_todo.py
├── meeting.txt
└── test_todo.py

第 1 步:代码

meeting_todo.py:

"""会议纪要 → 待办清单(Structured Output:response_format=json_schema),再导出成日历文件 .ics。
程序会核对:负责人必须在纪要里出现过;日期必须是合法的 YYYY-MM-DD;没有明确日期的写 null,不许模型猜。
环境变量:MOONSHOT_API_KEY、KIMI_BASE_URL(默认 https://api.moonshot.cn/v1)、KIMI_MODEL(默认 kimi-k2.6)
用法:python meeting_todo.py meeting.txt [todo.ics]"""
import json, os, sys, uuid
from datetime import date, datetime, timezone
from openai import OpenAI

client = OpenAI(base_url=os.environ.get("KIMI_BASE_URL", "https://api.moonshot.cn/v1"),
                api_key=os.environ.get("MOONSHOT_API_KEY") or "missing-key")
MODEL = os.environ.get("KIMI_MODEL", "kimi-k2.6")
SCHEMA = {"type": "object", "additionalProperties": False, "required": ["todos"], "properties": {"todos": {"type": "array", "items": {
    "type": "object", "additionalProperties": False, "required": ["owner", "task", "due"],
    "properties": {"owner": {"type": "string"}, "task": {"type": "string"},
                   "due": {"type": ["string", "null"], "description": "YYYY-MM-DD;纪要里没有明确日期就填 null"}}}}}}

def extract(text, year):
    r = client.chat.completions.create(model=MODEL, temperature=0, messages=[
        {"role": "system", "content": f"从会议纪要中提取有明确负责人的待办事项,输出 json。今年是 {year} 年。"
                                      "写明了几月几日的,due 填对应日期;“下周一”这类相对日期不要自己换算,原样写进 task,due 填 null。没有负责人的事项不要输出。"},
        {"role": "user", "content": text}],
        response_format={"type": "json_schema", "json_schema": {"name": "todos", "strict": True, "schema": SCHEMA}})
    return json.loads(r.choices[0].message.content)["todos"]

def fill_explicit_dates(todos, year):
    """把日期统一成 YYYY-MM-DD:模型把 due 写成“10月28日”、或者没填但任务里写着明确日期,都由程序换算。
    “下周一”这类相对日期不处理,保持待定。"""
    import re
    for t in todos:
        try:
            if t["due"]: date.fromisoformat(t["due"]); continue
        except ValueError: pass
        m = re.search(r"(\d{1,2})\s*月\s*(\d{1,2})\s*日", f"{t['due'] or ''} {t['task']}")
        t["due"] = f"{year}-{int(m[1]):02d}-{int(m[2]):02d}" if m else None
        if m: t["filled"] = True
    return todos

def check(todos, text):
    problems = []
    for t in todos:
        if t["owner"] not in text: problems.append(f"负责人“{t['owner']}”不在纪要里")
        if t["due"]:
            try: date.fromisoformat(t["due"])
            except ValueError: problems.append(f"日期“{t['due']}”不是 YYYY-MM-DD"); t["due"] = None
    return problems

def to_ics(todos):
    now = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    ev = [f"BEGIN:VEVENT\r\nUID:{uuid.uuid4()}\r\nDTSTAMP:{now}\r\nDTSTART;VALUE=DATE:{t['due'].replace('-', '')}\r\n"
          f"SUMMARY:{t['owner']}:{t['task']}\r\nEND:VEVENT" for t in todos if t["due"]]
    return "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//meeting_todo//CN\r\n" + "\r\n".join(ev) + "\r\nEND:VCALENDAR\r\n"

if __name__ == "__main__":
    text = open(sys.argv[1], encoding="utf-8").read()
    todos = fill_explicit_dates(extract(text, date.today().year), date.today().year)
    for p in check(todos, text): print("⚠", p)
    for t in todos: print(f"- [{t['due'] or '日期待定'}] {t['owner']}:{t['task']}" + ("(日期由程序从原文补全)" if t.get("filled") else ""))
    if len(sys.argv) > 2:
        open(sys.argv[2], "w", encoding="utf-8", newline="").write(to_ics(todos))
        print(f"已写入 {sys.argv[2]}({sum(1 for t in todos if t['due'])} 个有日期的待办,可导入手机或电脑日历)")

meeting.txt:

10 月 21 日 周会纪要(虚构示例)
参会:王敏、陈晨、刘洋
1. 官网改版:陈晨负责在 10 月 28 日前出首页设计稿,王敏周五(10 月 24 日)前确认文案。
2. 客户回访:刘洋下周一联系三家重点客户,整理反馈。
3. 年会场地:暂未定,下次周会再讨论。

test_todo.py:

import os; os.environ.setdefault("MOONSHOT_API_KEY", "test")
from meeting_todo import check, to_ics

def test_owner_and_date_checks():
    todos = [{"owner": "赵六", "task": "x", "due": "2026-13-01"}]
    p = check(todos, "参会:王敏")
    assert len(p) == 2 and todos[0]["due"] is None

def test_ics_only_dated():
    ics = to_ics([{"owner": "王敏", "task": "确认文案", "due": "2026-10-24"}, {"owner": "刘洋", "task": "回访", "due": None}])
    assert ics.count("BEGIN:VEVENT") == 1 and "DTSTART;VALUE=DATE:20261024" in ics

def test_fill_explicit_dates():
    from meeting_todo import fill_explicit_dates
    t = fill_explicit_dates([{"owner": "陈晨", "task": "10 月 28 日前出设计稿", "due": None},
                             {"owner": "刘洋", "task": "下周一联系客户", "due": None}], 2026)
    assert t[0]["due"] == "2026-10-28" and t[1]["due"] is None

def test_due_written_in_chinese_is_normalized():
    from meeting_todo import fill_explicit_dates
    assert fill_explicit_dates([{"owner": "王敏", "task": "确认文案", "due": "10月24日"}], 2026)[0]["due"] == "2026-10-24"

第 2 步:运行

$ KIMI_BASE_URL=http://127.0.0.1:11434/v1 KIMI_MODEL=qwen2.5-local:1.5b python meeting_todo.py meeting.txt todo.ics
- [2026-10-28] 陈晨:出首页设计稿(日期由程序从原文补全)
- [2026-10-24] 王敏:确认文案(日期由程序从原文补全)
- [日期待定] 刘洋:联系客户,整理反馈
已写入 todo.ics(2 个有日期的待办,可导入手机或电脑日历)

检查点:“下周一”没有被换算成具体日期,这是有意的:相对日期由人来确认,程序和模型都不去猜。“年会场地”在纪要里是“暂未定”,所以没有生成待办。

调试示例:日期写进了任务描述里

一开始,模型有时把“10 月 28 日前”写进 task 字段,due 字段却是 null,日历里就少了这个事件。fill_explicit_dates 现在同时在 due 和 task 里找“X月X日”格式的明确日期,按纪要的年份补全,并注明“由程序从原文补全”。

常见问题

现象 原因 处理
负责人是纪要里没出现过的人 模型编造 程序核对后丢弃,并提示
导入日历后时间不对 全天事件的格式写错 本文使用 DTSTART;VALUE=DATE
漏掉了待办 小模型能力有限 换大模型,并人工对照纪要

模板规则

  • 用 schema 约束结构,用程序核对内容。
  • 日期只取原文里写明的,相对日期留给人确认。
  • 导出前列出全部待办,让人过目。

项目资源

用途 地址
response_format(Structured Output) https://platform.kimi.com/docs/guide/response_format
JSON Mode https://platform.kimi.com/docs/guide/use-json-mode-feature-of-kimi-api
iCalendar 规范 RFC 5545 https://www.rfc-editor.org/rfc/rfc5545

从纪要到日历只差一个 schema 和几道核对,省下的是会后手工整理待办的时间。