上周整理备忘录的时候,我翻出三百多条记了快两年的碎片笔记:有会议纪要、随手拍的产品灵感、去菜市场记的买菜清单、半夜冒出来的奇怪想法。说实话,大部分内容我自己都懒得回看,因为时间一长根本分不清哪条有用。于是我想了个办法——用Python给备忘录接上AI摘要和文本润色能力,让每一条入库的文本自动生成一段摘要,顺手把病句和错别字处理掉。这篇文章就是一个完整的实战记录:Bmob后端云负责存数据,Python负责调用大模型接口做摘要和润色,最终形成一条"笔记写入→AI自动处理→结果回写"的流水线。适合正在做笔记类App、想给现有业务塞进AI能力、或者单纯想把个人笔记整理流程自动化的朋友参考。
1. 需求拆解:AI能力应该放在哪一层,而不是急着写代码
很多人一听到"给备忘录接AI",第一反应就是直接把大模型SDK塞进客户端,用户点一下按钮,客户端拿着全文去调API,等返回结果再显示。这个方案在Demo阶段跑得通,但稍微想深一点就会发现问题:API密钥放在客户端等于裸奔,反编译一下就能拿走;每台设备都在用各自的账号调大模型,费用和限额根本没法统一管理;如果备忘录还要做多端同步,客户端直连大模型的方式会让每一端都重复实现一遍逻辑。
所以在动手写之前,我花时间想清楚了一件事:AI能力到底应该放在哪一层。这里有三条路可以走。
方案一:客户端直连大模型。优点是最简单,缺点也最明显。密钥安全、调用统计、多端一致性全是坑。只适合个人自用的小工具,不适合要上架或者多人协作的项目。
方案二:全部写在Bmob云函数里。Bmob作为后端云平台,确实提供了云函数能力,可以在服务端写Node.js代码,由客户端通过REST API触发。这个方案的优点是省去了自己买服务器的麻烦,Bmob帮我们扛了服务端的基础设施。但有一个现实问题:Bmob云函数用的是Node.js环境,如果项目团队主语言是Python,或者你后续想把AI处理逻辑沉淀成一套Python组件库,那所有代码都得用JavaScript重写一遍,维护成本会翻倍。
方案三(本文采用):Bmob管存储,Python管AI逻辑。数据层交给Bmob,笔记的增删改查、多端同步、历史版本都走Bmob的REST API;单独起一个Python服务(也可以就是一个定时脚本),负责从Bmob拉取待处理的笔记,调用大模型接口,再把AI结果写回Bmob。这样Python生态里各种文本处理工具、定时任务框架、监控告警能力全都能直接用上,密钥也只在服务端出现。
对比一下这三种方案,差异还是很明显的:
| 维度 | 客户端直连 | Bmob云函数 | Bmob存储 + Python服务 |
|---|---|---|---|
| 实现难度 | 最低 | 中等 | 中等偏高 |
| 密钥安全 | 差 | 好 | 好 |
| 多端一致性 | 差 | 好 | 好 |
| Python技术栈复用 | 无 | 无 | 完整 |
| 服务端资源成本 | 无 | 平台承担 | 需要一台小服务器 |
| 适合场景 | 个人原型 | 纯JS团队 | Python团队/复杂AI逻辑 |
如果你的团队主要写JavaScript,方案二完全可行。但我要做的是把摘要、润色做成一套可扩展的AI处理管线,后面可能还会接标签自动分类、关键词提取,所以选了方案三。道理很简单:工具应该迁就你的核心逻辑,而不是让核心逻辑去迁就工具。
2. Bmob数据表设计与Python客户端封装
2.1 数据表字段设计:不止是标题和正文
Bmob的数据存储用起来和传统数据库不太一样,不需要预先在控制台画表结构,直接在代码里往类名(相当于表)里塞字段就行,第一个对象创建时字段就自动带出来了。但这不代表可以随便设计,尤其是要给AI功能预留状态位的时候。
我建了一个名为Note的类,字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
title | String | 笔记标题,允许为空 |
content | String | 笔记正文 |
summary | String | AI生成的摘要,处理完成后写入 |
polishedContent | String | 润色后的正文 |
status | String | pending/processing/done/failed |
errorMsg | String | 处理失败时的错误信息,方便排查 |
needProcess | Boolean | 是否需要走AI处理流程 |
status和needProcess这两个字段是整条流水线的灵魂。用户可能新建一条笔记,也可能编辑一条旧笔记,不管哪种情况,只要内容有变化,就把status置为pending、needProcess置为true。Python服务不关心笔记是怎么来的,只认这两个字段,轮询到待处理的数据就开工。这样设计的好处是:客户端写数据和服务端处理数据完全解耦,用户写下笔记立刻就能关掉页面,AI处理在后台慢慢跑,不需要用户干等。
2.2 用requests封装Bmob REST API
Bmob官方没有特别维护Python SDK,但它的REST API非常规整,用requests库封装一层就能用得很顺手。核心配置就三样:Application ID、REST API Key、接口地址https://api.bmob.cn/1。
import json import requests class BmobClient: """Bmob REST API 轻量封装""" def __init__(self, app_id: str, rest_api_key: str): self.base_url = "https://api.bmob.cn/1" self.headers = { "X-Bmob-Application-Id": app_id, "X-Bmob-REST-API-Key": rest_api_key, "Content-Type": "application/json", } def create(self, table: str, data: dict) -> dict: resp = requests.post( f"{self.base_url}/classes/{table}", json=data, headers=self.headers, timeout=15, ) resp.raise_for_status() return resp.json() def get(self, table: str, object_id: str) -> dict: resp = requests.get( f"{self.base_url}/classes/{table}/{object_id}", headers=self.headers, timeout=15, ) resp.raise_for_status() return resp.json() def update(self, table: str, object_id: str, data: dict) -> dict: resp = requests.put( f"{self.base_url}/classes/{table}/{object_id}", json=data, headers=self.headers, timeout=15, ) resp.raise_for_status() return resp.json() def query(self, table: str, where: dict, limit: int = 10, order: str = "-updatedAt") -> list: params = { "where": json.dumps(where, ensure_ascii=False), "limit": limit, "order": order, } resp = requests.get( f"{self.base_url}/classes/{table}", params=params, headers=self.headers, timeout=15, ) resp.raise_for_status() return resp.json().get("results", [])这套封装基本覆盖了笔记类应用90%的数据操作需求。create用来写笔记,query用来捞待处理的数据,update用来回写AI结果。Bmob查询时where参数必须是一个JSON字符串,不能直接传Python字典,所以json.dumps(where, ensure_ascii=False)这行不能省,否则中文条件会查询不到数据。
2.3 封装中容易翻车的细节
用Bmob REST API的过程中,有几个细节值得单独拿出来说,这些都是在官方文档里容易忽略的地方。
第一个坑是Content-Type请求头。某些HTTP客户端库会自动加Charset,导致Bmob服务端解析中文时出现乱码。我在用requests时直接用requests.put(json=data, headers=headers),requests会自己处理好编码,只要别手动把json参数改成data就行。
第二个坑是Bmob内置的时间字段。Bmob默认给你带上createdAt和updatedAt,但它们是ISO 8601格式的字符串,不是时间戳。做排序时用order=-updatedAt没问题,但如果想按某个自定义字段做范围查询,需要特别注意值类型。
第三个坑是权限。Bmob创建应用后默认的安全规则比较宽松,所有客户端都能读写。跑通流程之后,一定要在控制台把数据表的权限改为登录用户才能访问,或者至少给AI服务分配独立密钥,否则别人拿着你的X-Bmob-Application-Id和X-Bmob-REST-API-Key就能把整张表拖走。API Key泄露和数据库密码泄露没什么分别。
3. AI摘要和润色的核心逻辑:提示词设计与容错
3.1 大模型API统一适配层
Python这边接大模型,我的习惯是先做一层薄薄的适配,而不是在业务代码里到处写requests.post。目前主流的国内大模型平台,比如DeepSeek、通义千问、智谱,大多数都提供OpenAI兼容的接口格式,所以统一封装并不复杂。
class LLMClient: def __init__(self, api_key: str, base_url: str, model: str): self.api_key = api_key self.base_url = base_url.rstrip("/") self.model = model def chat(self, messages: list, temperature: float = 0.3, max_tokens: int = 1024) -> str: headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } payload = { "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } resp = requests.post( f"{self.base_url}/chat/completions", json=payload, headers=headers, timeout=60, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"].strip()这里把模型名、接口地址、API Key通过构造参数传进来,换模型平台的时候只需要改配置,不用动业务逻辑。当前我用的是DeepSeek的接口,base_url填的是它的API地址,模型填的也是对应模型名称。如果你想换通义千问,只需要把地址和模型名换掉,这个适配层不需要改。
3.2 摘要提示词:控制长度是关键
摘要功能看上去简单,真正调的时候才发现,大模型很容易把"摘要"理解成"重写"——一旦你让模型自由发挥,它可能返回一段比原文还长的"概括",或者用一堆书面语把原来的口语风格抹掉。我试了几版提示词,最终稳定下来的是这一版:
SUMMARY_PROMPT_TEMPLATE = ( "你是一个笔记整理助手。请为下面的笔记生成摘要。\n" "要求:\n" "1. 字数不超过100字;\n" "2. 保留原文中的关键信息和结论;\n" "3. 使用简洁的中文表述;\n" "4. 不要输出任何开场白、解释或评论;\n" "5. 直接输出摘要正文。\n\n" "笔记内容:\n{content}" ) def generate_summary(llm: LLMClient, content: str) -> str: if len(content.strip()) < 50: return content.strip() prompt = SUMMARY_PROMPT_TEMPLATE.format(content=content[:3000]) return llm.chat([{"role": "user", "content": prompt}], temperature=0.3, max_tokens=300)注意几个细节。第一,对内容长度做了截断,超出3000字的部分先不喂给模型,避免上下文过长拖慢响应时间,也避免无意义的费用消耗。第二,小于50字的笔记直接返回原文,因为这种长度做摘要没有意义,还能省一次API调用。第三,temperature=0.3让模型输出更稳定,摘要这种任务不需要创造性,温度越低越好。
3.3 润色提示词:让AI做编辑,而不是做作者
润色比摘要更容易失控。如果你只写一句"请润色以下文本",模型大概率会把原文里一些细节删掉,甚至擅自补充内容。比较好的做法是明确告诉模型:你是编辑,不是作者,不许改原意,不许增删事实。
POLISH_PROMPT_TEMPLATE = ( "你是一个文字编辑。请对下面的文本进行润色。\n" "要求:\n" "1. 修正错别字、标点错误和不通顺的句子;\n" "2. 优化表达,使逻辑更清晰;\n" "3. 保持原文内容和语气不变,不得新增或删减事实;\n" "4. 保留原文的段落结构;\n" "5. 直接输出润色后的完整文本,不要任何说明。\n\n" "文本内容:\n{content}" ) def polish_content(llm: LLMClient, content: str) -> str: prompt = POLISH_PROMPT_TEMPLATE.format(content=content[:5000]) return llm.chat([{"role": "user", "content": prompt}], temperature=0.2, max_tokens=2048)润色的max_tokens比摘要大很多,因为要完整返回原文长度的文本。如果你的笔记经常超过几千字,建议先做分段润色再合并,不然要么超时,要么返回被截断。我后来偷懒用了个简单策略:超过2000字的笔记只润色前面2000字,并在结果后面加一行"(长文本已截断处理)"。这个策略虽然粗暴,但保证了流程不会卡在某一条超长笔记上。
3.4 返回结果解析的容错
调用大模型接口时,我最担心的是响应体格式不对。有的平台在返回内容里混入Markdown标记,有的平台会返回一段带json代码块的文本,还有的平台偶尔会返回空结果。所以我在适配层做了归一化:
def _safe_extract_content(data: dict) -> str: try: content = data["choices"][0]["message"]["content"] except (KeyError, IndexError, TypeError): raise RuntimeError(f"unexpected response: {json.dumps(data, ensure_ascii=False)[:200]}") if not content: raise RuntimeError("empty content from llm") return content.strip()这样抛出来的异常都带上原始响应的片段,排错的时候一眼就能看出问题在哪个环节。
再补充一个容易被坑的地方:大模型输出通常用UTF-8,但如果你在Windows上用IDE跑Python脚本,把结果写回Bmob时可能会因为编码问题报错。处理方式是确保所有网络请求都显式声明UTF-8:写文件时用encoding="utf-8",打印日志时也统一UTF-8。这个细节不处理,在Windows服务器上部署迟早会踩中。
4. 完整处理流水线:新增、编辑、历史数据三种场景
4.1 实时处理:客户端写入后立即调用
整套流程我设计成三个角色:备忘录客户端(或Python脚本)、Bmob数据层、Python AI处理服务。
用最典型的场景举例:用户新建了一条笔记"明天下午三点和张三开会讨论项目排期,地点在301会议室,记得带上次的测试报告"。
第一步,客户端把这条笔记写入Bmob:
note_id = bmob.create("Note", { "title": "项目会议", "content": "明天下午三点和张三开会讨论项目排期,地点在301会议室,记得带上次的测试报告。", "status": "pending", "needProcess": True, })["objectId"]第二步,Python处理服务定时轮询(我用的是schedule库,每30秒扫一次),或者收到通知后立即处理。处理函数长这样:
def process_note(note_id: str): note = bmob.get("Note", note_id) if note.get("status") == "processing": return bmob.update("Note", note_id, {"status": "processing"}) try: content = note.get("content", "") summary = generate_summary(llm, content) polished = polish_content(llm, content) bmob.update("Note", note_id, { "summary": summary, "polishedContent": polished, "status": "done", "needProcess": False, }) except Exception as exc: bmob.update("Note", note_id, { "status": "failed", "errorMsg": str(exc)[:200], })第三步,客户端在查询笔记详情时,如果发现status是done,就展示summary和polishedContent;如果还是processing或pending,就展示原文,并提示"AI处理中"。
这套流程里最关键的是把状态先置成processing再开始处理,防止多个worker同时处理同一条笔记。
4.2 历史笔记批量补齐
大部分人接手这类需求时,库里已经躺着一堆旧数据了。旧笔记的summary、polishedContent都是空的,status也从来没设置过。写一个批量补处理脚本就行:
def backfill_pending_notes(batch_size: int = 10): while True: notes = bmob.query("Note", {"needProcess": True}, limit=batch_size) if not notes: break for note in notes: process_note(note["objectId"])这里有个经验:批量处理时不要一次全捞出来,每次取10条处理完再取下一批。一方面大模型API有并发限制,一次性开几十个并发容易被限流;另一方面,万一某条笔记的内容有问题导致处理失败,小批次处理能更快定位到问题数据。
4.3 幂等设计:为什么状态机这么重要
我刚开始跑流水线时犯过一个错:处理完一条笔记后,直接更新summary和polishedContent,没有管状态字段。结果定时任务下一次扫描时,发现这条笔记的needProcess还是true,又把它拉出来处理了一遍,白花了一次API的钱。
后来我把状态机补完整:
- 写入时:
status=pending,needProcess=true - 处理中:
status=processing,防止重复消费 - 成功:
status=done,needProcess=false - 失败:
status=failed,保留errorMsg,needProcess=true
失败后还能重试,这正是needProcess存在的意义。我额外写了个失败重试机制:处理失败的笔记30分钟后重新置为pending,最多重试3次,超过3次就进入人工排查列表。
幂等设计的本质是让每一条数据在任何时刻都只有一个明确的处理状态。这个原则不仅适用于Bmob,换成MySQL还是MongoDB都一样。
5. 实测踩坑:超时、并发、截断与脏数据
5.1 大模型响应慢导致接口超时
刚开始我把requests.post的timeout设成15秒,结果长度超过1500字的笔记,摘要加润色两个接口跑下来经常超过30秒,直接把整个处理脚本卡超时。后来把大模型请求的超时时间放宽到60秒,同时把单条笔记的完整处理链路改成异步任务。如果你的笔记普遍偏长,建议把摘要和润色拆成两个独立队列,甚至可以并行跑,但要注意大模型接口的并发限制。
5.2 并发触发导致同一笔记被处理两次
有一次我手动测试时,同时开了两个Python进程去跑定时任务,结果同一条笔记被两个进程同时拉出来,各调了一次大模型API。因为两个进程都在更新summary,后写入的覆盖了先写入的,看起来结果没错,但API费用白白翻倍。
解决方式就是我前面说的状态机:处理前先更新status=processing,处理结束后才更新为done。但这套机制有一个前提:更新processing和后续读取内容之间不能有太大的时间差,否则在分布式场景下仍然可能撞车。如果进程再多一点,可以考虑给记录加锁字段,或者用Bmob的原子操作来确保只有当前状态为pending的记录才能被更新为processing。
5.3 大模型返回内容被截断
摘要还比较少出现这个问题,润色的max_tokens如果设太小,返回的文本会在中间被硬生生切断,最后可能留下半句话。我遇到最典型的一次,润色5000字的内容时max_tokens只给了1024,结果返回的文本在几百字处戛然而止。
解决方式:长文本先按段落切分,分段润色后再拼接。切分时不要按固定长度切,尽量按换行符切,避免把一个段落拦腰截断。拼接时还要注意段落之间保留一个空行,否则润色结果会挤在一起。
5.4 全文标点引发的JSON解析失败
还有一个很隐蔽的坑。某条笔记里有一堆特殊字符,包括繁体中文、全角括号、甚至一些控制字符。把这些内容直接塞进JSON请求体时,虽然requests库能正确编码,但大模型接口那边偶尔会把特殊字符转义出错,返回一段无法解析的JSON。这时候我的适配层就会抛出刚才那个unexpected response异常,然后整条笔记被标记为failed。
排查下来发现,问题往往出在原文的某些不可见控制字符,比如\u0000之类的。处理办法是在入库前做一次字符清洗,把控制字符过滤掉:
import re def clean_text(content: str) -> str: return re.sub(r"[\x00-\x1f\x7f]", "", content)这个过滤函数很便宜,但能省掉后面一堆解析问题。建议在客户端写入数据、Python处理数据这两处都加上。
5.5 测试建议:准备一份覆盖各种情况的测试语料
实测经验告诉我,不要拿真实笔记去开荒。我专门准备了一份测试语料,涵盖常见的边界情况:空内容、纯符号、超长文本、带Markdown表格的笔记、包含代码片段的笔记、纯英文内容。每次改完提示词或处理逻辑,先跑这份测试语料,确认全部通过后再放量。这样能省掉很多调试时间。
6. 后续还能怎么扩展
这套架构跑通之后,扩展AI能力其实变得很顺手。因为Python服务已经和Bmob数据层打通了,再往Note表加字段、往处理管线加新的AI步骤,成本都很低。
我目前正在试的两个方向:一个是标签自动分类,让模型根据笔记内容生成3到5个关键词,同时写入tags字段,方便后续按标签筛选;另一个是定时回顾,每天晚上把当天新增的笔记摘要汇总成一条"每日回顾",推送到自己的IM工具里,相当于给自己做了一个自动化知识管理助手。如果你也想做类似的事情,不需要把第一步搞得多复杂,先把"Bmob存储 + Python处理 + AI回写"这条最小链路跑通,后面的想象力自然就打开了。