第 1 课:谁拥有便签
上一页已经跑出了新便签。现在暂时把模型放到一旁,先回答:新便签究竟存在哪里?如果这件事没有明确的所有者,就很容易把模型的文字回答、工具的返回值和真实数据混为一谈。
从一个普通业务对象开始
export class Notes {
id = randomUUID();
#revision = 0;
#items = new Map();
get version() {
return String(this.#revision);
}
keys() {
return [...this.#items.keys()];
}
read(key) {
if (!this.#items.has(key)) throw new RangeError('Note does not exist');
return this.#items.get(key);
}
append(value) {
if (!value.title || !value.text) throw new TypeError('A note requires title and text');
const item = stableValue({ id: randomUUID(), value });
this.#items.set(item.id, item);
this.#revision++;
return item;
}
}Notes 拥有一个私有 Map。append 生成便签 ID、安装内容、递增集合版本;read 按 ID 取材料,找不到就抛出错误。类自身也有 ID,用来标识这一份便签集合。集合身份与其中每张便签的身份是两回事。
它没有继承框架基类。框架不替业务决定“便签应该是什么”,也不要求每个业务对象先接入 Runtime。这里的材料形状是 { id, value },value 含标题与正文;后面 Node 和工具反馈都沿用这个真实形状。
关注 append 的先后顺序:先以 stableValue 固定材料,再放入 Map。调用者随后修改传入的普通对象,不会改写已安装的便签;读取返回的固定内容也不能被直接篡改。这个稳定性让后续步骤能根据已经收到的反馈继续工作。
version 是整个集合的变更标记。它不表示这个示例保存了可查询的历史;Map 只保存当前这些便签。固定值与持久化也是两件事:固定的是内容,本例仍只有内存。
为同一次任务准备材料
export function createMaterials(values = initialValues) {
const notes = new Notes(); // 仅内存,不落盘,不设置 checkpoint。
const originals = values.map((value) => notes.append(value));
const request = {
messages: [
{ role: 'system', content: '你是便签助手。用 readNote 阅读用户提供的两张便签,整理出三项待办,用 writeNote 创建一张标题为“待办”的新便签。确认写入成功后回答新便签标题和完整内容。不要修改原便签,不要声称保存到磁盘。' },
{ role: 'user', content: JSON.stringify({ task: '读取这两张便签,生成待办便签并回答。', notes: originals.map(({ id, value }) => ({ id, title: value.title })) }) },
],
};
return { notes, originals, request };
}createMaterials 是宿主的准备工作。它创建 Notes,再把初始值追加为两张真实便签。返回的 originals 是这两次写入的结果,因而用户请求中的 ID 确实能在同一个 Notes 中找到。
观察 user 消息:它只给模型原便签的 ID 和标题,没有直接塞入正文。模型要拿正文,就得提出 readNote 调用。这让后面的读取步骤有实际意义,也让我们可以核对助手是否真的访问了两份材料。
system 消息说明任务目标,真正的数据仍在 Notes。把提示写成“不要修改原便签”有助于表达期望;本例提供的写操作本身只会追加,也没有修改既有便签的工具。
练习:改动真实材料
初始值也直接来自源码:
export const initialValues = [
{ title: '会议记录', text: '整理用户反馈;发送演示链接' },
{ title: '项目计划', text: '检查安装步骤' },
];将「检查安装步骤」改成「确认参会名单」,保留两张便签和总计三条事项,再运行:
node examples/notes-assistant.mjs预期第二次读取、新便签正文和最终回答都出现「确认参会名单」。这说明结果经过了“实体材料 → 读取反馈 → 新便签”的路径。离线脚本按分号拆分事项;此次练习只换材料,不改变任务协议。练习后还原这一处文字,方便与后续默认结果对照。
你现在已经能解释内容归谁所有。下一步是决定谁能怎样接触它:进入第 2 课:把操作交给资源。实体设计的更多能力见自定义实体。