我把 AI 总结笔记从 3 次模型调用降到 2 次,结果反而更准了
用户只说了一句话:
把我今天关于发布计划的笔记,以及还没完成的相关待办,整理成一篇复盘。
这类需求看起来很适合交给 AI:理解一句自然语言,找到个人工作区中的资料,再生成一篇结构完整的笔记。
第一版链路通常会写成:
判断用户要不要生成笔记
↓
让 Planner 决定查询哪些资料
↓
执行查询
↓
把材料交给模型生成草稿
一共需要 3 次模型调用。
我原本也觉得这很合理:每个模型只做一件事,职责清楚,复杂任务就多规划一轮。
真正接入真实笔记、书签、文件和待办后,我却发现一个反直觉的问题:
模型调用越多,不一定理解得越充分,反而可能把同一个材料范围解释两遍,并在第二遍悄悄改掉。
于是我把链路改成了 2 次:
第 1 次:同时判断任务类型,并输出结构化材料范围
↓
服务端确定性执行只读查询
↓
第 2 次:根据权威材料生成待确认草稿
减少一轮后,延迟和 Token 自然下降;更重要的是,材料范围只被模型解释一次,查询行为也变得可测试。
本文就从这句“总结我今天的笔记”开始,拆解一套可以复用的 AI 知识整理链路。

一、问题不在生成,而在“今天的笔记”到底是什么
当用户说:
总结我今天的笔记。
人很容易觉得查询条件已经很明确了。
但后端真正需要的是:
{
"resourceType": "note",
"timeRange": "今天"
}
如果用户改成:
把我今天关于发布计划的笔记,以及还没完成的相关待办,整理成一篇复盘。
材料范围就至少包含:
[
{
"resourceType": "note",
"keyword": "发布计划",
"timeRange": "今天"
},
{
"resourceType": "todo",
"keyword": "发布计划",
"todoStatus": "pending"
}
]
这已经不是普通关键词搜索,而是一组包含资源类型、主题、时间和状态的查询计划。
更麻烦的是,同一句话里还混合了两个不同目标:
- 读取工作区中的真实材料;
- 生成一篇可保存的新笔记。
如果直接把整句交给一个通用 Agent,它既要猜材料范围,又要选择工具,还要决定什么时候写数据库。表面上只发了一条消息,内部却把检索、写作和写入权限全部绑在一起。
二、为什么 3 次调用反而容易范围漂移
旧链路中的 3 次模型调用分别是:
调用 1:这是问答,还是要产出一篇笔记?
调用 2:为了完成任务,应该查询哪些资料?
调用 3:根据查询结果生成正文
单看每一步都合理,问题出在调用 1 和调用 2 都会读取原始用户消息,并各自解释一次。
例如第一次分类得到:
需要查询“今天关于发布计划的笔记”和“未完成待办”
第二次 Planner 可能重新理解成:
查询最近几天的全部笔记,再查询最新待办
这种变化通常不会导致接口报错。查询仍然成功,模型也能写出一篇语言流畅的复盘。
真正危险的地方是:结果看起来合理,但材料范围已经不再是用户要求的范围。
多一轮推理还会带来其他成本:
- 多一次 Provider 延迟;
- 多一份系统 Prompt;
- 多一轮上下文序列化;
- 多一次 Tool Schema 选择;
- 多一组失败重试与追踪状态;
- Planner 输出无法完整映射时,还需要额外澄清。
因此,优化目标不能只写成“减少模型调用次数”,而应该是:
让材料范围只经过一次非确定性解释,之后全部由确定性代码执行。
三、把任务判断和材料范围放进同一个分类协议
新的第一轮模型调用不再只返回一个布尔值,而是通过闭合 Tool Schema 一次输出四类信息:
{
"producesNote": true,
"otherMutations": false,
"needsWorkspaceRetrieval": true,
"workspaceQueries": [
{
"resourceType": "note",
"keyword": "发布计划",
"timeRange": "今天"
},
{
"resourceType": "todo",
"keyword": "发布计划",
"todoStatus": "pending"
}
]
}
producesNote
用户是否真的要求产出一篇可以保存的笔记。
下面两句看起来相近,但结果不同:
总结这些材料,告诉我重点。
把这些材料总结成一篇新的笔记。
第一句是只读问答,第二句才进入笔记草稿链路。
otherMutations
用户是否同时要求了其他写操作。
例如:
生成一篇复盘,再创建一个明天提醒我查看的待办。
这不是单一笔记任务,应该交回通用 Planner,不能让专用草稿链路悄悄忽略后半句。
needsWorkspaceRetrieval
已有显式选择的材料是否足够。如果用户说的是“我今天的笔记”而没有选中具体资源,就必须先查询工作区。
workspaceQueries
把材料范围转换成 1~4 个结构化只读查询,支持的资源类型包括:
note / bookmark / file / todo
这一步的价值在于:让分类结果不仅说明“需要查询”,还完整说明“查什么”。
如果只返回 needsWorkspaceRetrieval: true,后面还要再调用一次 Planner 理解材料范围,链路又多了一层。

四、分类器只描述范围,服务端负责真正查询
结构化输出不能直接成为数据库查询。
服务端还要把它映射到已经存在、已经做过权限校验的只读工具:
function buildQueryCalls(queries) {
return queries.flatMap((query) => {
switch (query.resourceType) {
case 'note':
return [{
toolName: 'query_notes',
args: {
keyword: query.keyword,
timeRange: query.timeRange,
limit: 50,
},
}];
case 'bookmark':
return [{
toolName: 'query_bookmarks',
args: {
keyword: query.keyword,
timeRange: query.timeRange,
tag: query.tag,
limit: 50,
},
}];
case 'file':
return [{
toolName: 'query_files',
args: {
keyword: query.keyword,
type: query.fileType,
limit: 50,
},
}];
case 'todo':
return [{
toolName: 'query_todos',
args: {
keyword: query.keyword,
status: query.todoStatus ?? 'pending',
sort: 'newest',
limit: 50,
},
}];
default:
return [];
}
});
}
这里有三条重要边界。
1. 查询工具只能是只读工具
分类器说“需要工作区材料”,不代表它可以获得创建、修改或删除权限。
这一阶段只允许:
query_notes
query_bookmarks
query_files
query_todos
2. 用户身份不能由模型提供
模型只描述资源类型和筛选条件,userId、管理员上下文和资源主体由服务端会话注入。
任何模型输出的 userId 都不应该进入查询。
3. 查询上限由服务端决定
即使模型输出 limit: 100000,也不能允许它扫描整个账户历史。分类协议甚至没有必要暴露这个字段,服务端统一使用有界值。
五、不支持的条件,绝不能静默删除
结构化协议带来的一个常见错误是:字段无法映射时,删掉它继续执行。
假设分类器输出:
{
"resourceType": "todo",
"timeRange": "今天",
"todoStatus": "pending"
}
但当前 query_todos 只支持状态和关键词,并不支持创建时间范围。
错误处理是:
query_todos({
status: 'pending'
});
接口会正常返回,模型也会继续写作。
但用户要求的是“今天的未完成待办”,实际材料却变成了“全部未完成待办”。范围被悄悄扩大,而且整个链路没有任何报错。

更安全的策略是 fail-closed:
if (
query.resourceType === 'todo' &&
query.timeRange
) {
throw new UnsupportedQueryFilterError(
'todo.timeRange is not supported'
);
}
随后可以:
- 要求用户缩小或改写范围;
- 回到 Planner 选择其他可验证能力;
- 明确告知当前无法完整执行。
不要用更宽的数据集冒充正确结果。
AI 系统里最难排查的 Bug,往往不是请求失败,而是请求成功却回答了另一个问题。
六、第二次调用只负责写作,不再负责找资料
服务端完成只读查询后,第二次模型调用接收三类输入:
原始用户要求
查询得到的权威材料
明确的输出格式
然后只能调用一个工具:
submit_note_draft
协议可以非常小:
const DRAFT_TOOL = {
type: 'function',
function: {
name: 'submit_note_draft',
description:
'提交一篇完成的 Markdown 草稿,供用户确认。',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
title: {
type: 'string',
maxLength: 255,
},
content: {
type: 'string',
maxLength: 60000,
},
},
required: ['title', 'content'],
},
},
};

为什么还要强制 Tool Call,而不是让模型直接返回 Markdown?
因为闭合 Schema 能稳定保证:
- 标题和正文分离;
- 不出现多余执行参数;
- 正文长度有上限;
- 协议失败可以被识别;
- 普通聊天文本不会被误当成完整草稿。
additionalProperties: false 还能阻止模型夹带:
{
"userId": "...",
"parentId": "...",
"skipConfirmation": true
}
生成模型只拥有写作权,不拥有数据库权限。
七、对话历史不能代替权威材料
很多 Agent 为了节省查询,会直接使用历史消息中的内容。
这在普通问答中可能够用,在“把我的资料整理成一篇笔记”时却不可靠。
历史消息可能包含:
- 上一轮工具结果的摘要;
- 被截断的长正文;
- 模型自己生成的解释;
- 已经过期的标题;
- 用户对材料的口头描述。
这些内容适合帮助模型理解对话,却不应该替代个人工作区中的真实记录。
因此,我把上下文分成两类:
会话上下文
用于理解:
“再写详细一点”
“换成周报格式”
“把第二节移到前面”
权威材料
来自本轮明确执行的只读查询,用于支撑正文事实。
只有后者能被当成草稿材料。
这种区分还能避免一个常见循环:
模型上一轮总结
↓
下一轮把总结当原始资料
↓
再次压缩与改写
↓
内容逐渐偏离真实来源
八、材料必须有预算,不能把整个工作区塞给模型
查询正确也不意味着应该全部放进上下文。
一个真实账户可能返回几十篇笔记、上百个书签和大量待办。直接拼接会导致:
- Token 超限;
- 前部材料被截断;
- 模型注意力被低相关内容稀释;
- 长正文拖慢每次草稿修改;
- 私密字段进入不必要的日志或确认存储。
可以显式设置预算:
最多材料:12
材料总字符:28000
上一版草稿:24000
草稿正文:60000
超出预算时,系统必须做出明确选择:
- 按相关度和时间排序;
- 保留标题、类型、时间和必要正文;
- 截断时增加标记;
- 材料太多则要求缩小范围;
- 不假装当前草稿覆盖了全部历史。
确认记录也不需要保存完整材料正文副本。
只保存稳定引用和原始要求:
{
"sourceMessage": "总结我今天的发布计划",
"contextRefs": [
{ "type": "note", "id": "note-1" },
{ "type": "todo", "id": "todo-2" }
]
}
用户确认或要求修改时,再由服务端重新检查这些资源是否仍然存在、是否仍属于当前主体。
九、为什么少一次模型调用,准确率反而可能更高
把 3 次调用改成 2 次,提升并不来自模型突然变聪明,而来自系统减少了不必要的非确定性。
范围只解释一次
旧链路:
分类器理解一次
Planner 再理解一次
新链路:
分类器一次输出完整范围
服务端按结构执行
查询参数可测试
workspaceQueries 是 JSON,可以直接写测试验证:
expect(result.workspaceQueries).toEqual([
{
resourceType: 'note',
timeRange: '今天',
},
]);
自然语言 Planner 的自由文本计划很难做到同样稳定。
上下文更干净
去掉一轮 Planner 消息后,草稿模型不再看到一大段中间推理和工具选择说明,只看到用户要求与真实材料。
错误更容易定位
新链路只有三个主要错误面:
分类错
查询错
生成错
而不是分类、规划、二轮规划、工具选择、材料拼接和生成混在一起。
延迟和成本同步下降
少一次 Provider 调用,也就少一次网络往返、排队、首 Token 等待和用量计算。
这类优化最有价值的地方,是准确性、成本和可维护性没有互相交换,而是同时改善。
十、测试应该围绕“范围有没有变”
除了普通的成功用例,我更关注下面这些测试。
任务类型
“总结这些材料,告诉我重点”
→ producesNote=false
“整理成一篇新笔记”
→ producesNote=true
“生成笔记并创建待办”
→ otherMutations=true
结构化范围
“今天的笔记”
→ note + timeRange=今天
“最近 7 天、标签为 AI 的书签”
→ bookmark + timeRange + tag
“还没完成的发布待办”
→ todo + keyword + pending
不支持字段
todo + timeRange
→ 显式失败,不静默删除
note + tag
→ 当前工具不支持则拒绝映射
file + todoStatus
→ 拒绝
材料权限
模型不能指定 userId
跨账号资源不进入材料
已删除资源不从标题快照恢复正文
管理员只读上下文不能扩大查询主体
草稿协议
必须只有一个 submit_note_draft
缺少 title / content → 拒绝
出现额外字段 → 拒绝
普通文本回复 → 视为协议失败
真正需要验证的不是“模型能不能写出一篇像样的文章”,而是:
最终草稿是否严格建立在用户指定的那一组材料上。
十一、什么时候不值得做这项优化
这套两阶段链路并不适合所有场景。
只有一个明确选中文档
用户已经选中一篇文章并要求“改写为摘要”,不需要额外分类工作区范围,可以直接进入生成阶段。
纯聊天问答
用户只问“这些材料讲了什么”,返回一段回答即可,不应该为了可能保存而额外调用分类器。
真正的多步骤复合任务
用户要求查询、创建笔记、创建待办并修改标签时,专用两阶段链路表达不了全部依赖,应交给通用 Planner。
优化的前提不是“调用越少越好”,而是:
一个结构化协议已经完整替代了原本那轮推理
如果只是删除 Planner,却没有其他组件接住它的职责,系统只会变得更快地答错。
结语
AI 总结个人笔记时,最值得警惕的并不是文笔差,而是它写得非常像,却根本没有读到用户以为它读到的那些材料。
解决这个问题,不能只继续修改 Prompt。
更可靠的办法是把链路拆成:
模型一次解释范围
服务端确定性读取
模型基于权威材料写作
这次从 3 次调用降到 2 次,表面上是性能优化,实际减少的是一次材料范围的重新解释。
在 AI 应用中,少一次推理有时不仅更省,也更可信。
完整实现来自开源项目轻笺,可参考:
更多推荐

所有评论(0)