谁说的

知道是谁说的记忆。

人的记忆以人为单位:Bob 承诺了什么,你说过要做什么。给每条记忆打上说话者标签,你的智能体也能做到,在所有 Tablet 和 Scroll 模型上。

说话者和存储库一样是显式的。先注册,再以其名字保存。拼写错误绝不会悄悄变成一个新人。每个存储库起步可注册 50 人(会逐步提高),"me" 永远无需注册也不计数。

一个团队,三条记忆

一个存储库能让多个声音互不混淆。先注册一个人,每句话按说话者保存,之后按人提问。

add注册一次 Bob:POST /speakers,SDK 里是 add_speaker("Bob")。

存储库现在认识 Bob 了。50 人上限只在注册这里计数,store 调用绝不会返回上限错误。

BobBob 说截止日期推迟到周二。用 speaker "Bob" 保存。

这条记忆现在属于 Bob:每次搜索返回它时都会这样标注。

me你的助手承诺周五前交摘要。它自己的话用 speaker "me" 保存。

自己说的话也会被记住,而且 "me" 永远不占说话者上限。

ask之后问:"Bob 关于截止日期说了什么?" 用 speaker "Bob" 搜索。

只返回 Bob 的话。一个人的话绝不会变成另一个人的。

只需记住三条规则

  • "me" 是助手本身。不注册、不计数。保留字且仅小写有效:speaker: "Me""ME" 不会被隐式转换,而是返回 400 invalid_request_error
  • 上限在注册时计数:每库起步 50 人。超限注册返回 400 invalid_request_error,错误体带 speaker_limit: 50。用未注册的名字 store 同样返回 400 且不保存任何内容。用未注册的名字过滤搜索返回 404 not_found_error。请按状态码和字段分支,不要解析消息文本;上限将逐步提高。
  • 标签存在于每一次读取。搜索结果、recall 的长期上下文、engram 结果都带有说话者,模型始终知道手里的话是谁的。搜索时传 speaker 就只返回那个人的话;supersede 保留说话者,forget 一并删除。
  • 名字是 Unicode,任何语言都可以。さくら、Иван、하늘 都是有效的说话者,归属行为在所有语言中一致。匹配在去空格和 Unicode 规范化后完全一致,因此 Bobbob 是两个人。名字上限 80 个字符。
errors - verbatim
# POST /speakers past the limit
{ "type": "error",
  "error": { "type": "invalid_request_error",
             "message": "This store already has 50 registered speakers, ...",
             "speaker_limit": 50 } }

# store with an unregistered name → 400, nothing stored
{ "type": "error",
  "error": { "type": "invalid_request_error",
             "message": "speaker 'Bob' is not registered in this store. Register it first: ...",
             "speaker": "Bob" } }

# search filtered by an unregistered name → 404
{ "type": "error",
  "error": { "type": "not_found_error",
             "message": "speaker 'Bob' is not registered in this store.",
             "speaker": "Bob" } }

两点范围说明。speaker 附加在 add / store 上:add_turn 整体记住一次交流,按人标注和过滤来自显式带 speaker 保存的记忆。另外,会话段落(expand)是多条记忆的合成,因此不带标签;speaker 过滤总是返回原子级、带标签的记忆。 另外,与已存记忆语义足够接近的写入会被丢弃。判定依据是含义而非字面一致。这样的 store 返回 status "duplicate" 和明确的 note,不保存任何内容,也不附加说话者。仅在某个细节上不同于既有记忆的新事实("花生过敏"之后的"甲壳类过敏")同样会被丢弃,因此请读取 status,不要假定写入已生效。

我们用最严格的方式测试它:存入正文里完全没有名字的记忆,再按人回忆。归属来自说话者记录而不是文字匹配,所以在任何语言里表现一致。

如何使用

mem.add_speaker("Bob", user_id="alice")  # once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")
hits = mem.search("what did Bob say about the deadline?", user_id="alice", speaker="Bob")
mem.list_speakers(user_id="alice")
mem.remove_speaker("Bob", user_id="alice")  # memories stay, the tag goes
await mem.addSpeaker("Bob", "alice");  // once per person; "me" needs no registration
await mem.add("Bob said the deadline moved to Tuesday", "alice", { speaker: "Bob" });
await mem.add("I promised the summary by Friday", "alice", { speaker: "me" });
const hits = await mem.search("what did Bob say about the deadline?", "alice", 10, { speaker: "Bob" });
await mem.listSpeakers("alice");
await mem.removeSpeaker("Bob", "alice");  // memories stay, the tag goes
mem.add_speaker("Bob", "alice").await?;  // once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", "alice", json!({"speaker": "Bob"})).await?;
mem.add("I promised the summary by Friday", "alice", json!({"speaker": "me"})).await?;
let hits = mem.search_with("what did Bob say about the deadline?", "alice", 10, json!({"speaker": "Bob"})).await?;
mem.list_speakers("alice").await?;
mem.remove_speaker("Bob", "alice").await?;  // memories stay, the tag goes
curl -X POST https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # once per person

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"Bob said the deadline moved to Tuesday","metadata":{"speaker":"Bob"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did Bob say about the deadline?","speaker":"Bob"}'

curl "https://api.wontopos.com/api/v1/memory/speakers?user_id=alice" -H "X-API-Key: $WOS_API_KEY"

curl -X DELETE https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # memories stay, the tag goes
response
{ "memories": [
    { "content": "Bob said the deadline moved to Tuesday",
      "speaker": "Bob", ... } ] }
GET /speakers
{ "user_id": "alice",
  "speakers": [ { "speaker": "Bob", "memories": 2, "created_at": "2026-07-10T04:20:39Z" } ],
  "count": 1, "limit": 50 }

列表显示存储库认识的人及各自的记忆数与上限。移除只删除注册:那个人的记忆保留,只有名字标签消失。

读取某个人的记忆

by_speaker 无需查询词即可返回某个人说过的内容,按时间由新到旧。"me" 返回助手自己的话。分页方式与图像相同,采用游标:把 next_beforenext_skip_ids 回传。

page = mem.by_speaker("Bob", limit=50)
page["memories"], page["chunks"]
const page = await mem.bySpeaker("Bob", undefined, { limit: 50 });
let page = mem.by_speaker("Bob", None, 50, None, None).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/by-speaker \
  -H "X-API-Key: $WOS_KEY" \
  -d '{"user_id":"alice","speaker":"Bob","limit":50}'
字段作用
memories记忆列表,按时间由新到旧。结构与检索返回的相同。
chunks这些记忆背后的句子级片段,即一次删除实际会移除的内容。通常大于记忆的条数,建议在确认删除前先展示。同时以 points_to_delete 返回。
next_before下一页的游标,与 next_skip_ids 配合使用。两者都必须传,因为记忆可能共用同一时间戳。
此处的 speaker 是写入时记录的标签,而不是对文本的检索。存储时未指定说话人的记忆可以通过检索找到,但永远无法通过 by_speaker 取得,包括在 "me" 下。

列出、浏览与移除说话人

mem.list_speakers()                    # who is registered
mem.by_speaker("Bob")                 # what Bob said, newest first
mem.remove_speaker("Bob")             # unregister; the memories stay
await mem.listSpeakers();
await mem.bySpeaker("Bob");
await mem.removeSpeaker("Bob");
mem.list_speakers(None).await?;
mem.by_speaker("Bob", None, None, None, None).await?;
mem.remove_speaker("Bob", None).await?;
curl -X GET    .../api/v1/memory/speakers   -d '{"user_id":"alice"}'
curl -X POST   .../api/v1/memory/by-speaker -d '{"user_id":"alice","speaker":"Bob"}'
curl -X DELETE .../api/v1/memory/speakers   -d '{"user_id":"alice","speaker":"Bob"}'