personos-mem 记忆服务接口文档

personos-mem 是一个分层记忆服务:调用方(你的 App / Agent)把用户对话喂进来,它自动切分话题、抽取事实、连成记忆;需要时按问题检索并给出一段标准事实陈述作为作答依据,同时沉淀一份用户画像。本服务只负责「记忆」,对话的组织由调用方完成。

多模态:写入支持 文本 / 图片 / 视频 clip(见 /ingest 下的「视频 clip」一节);召回支持 文本 / 图片(见 /recall 下的「带图提问」一节)。视频会被看懂画面与对话、认出其中的人物并跨会话延续,之后用普通的文字提问就能召回;带图提问则能把"他""这家店"这类指代落成具体的人和事物。

环境与域名

环境 Base URL
SIT http://localhost:8000
Staging https://staging.example.com
Prod https://memory.example.com

所有业务接口前缀 /api/v1。下文路径均省略该前缀(如 /ingest = POST /api/v1/ingest)。

鉴权(两层)

调用本服务有两层身份,叠加使用:

  1. 调用方签名(AK/SK)——服务级鉴权,所有 /api/v1/* 请求必带。你会拿到一对 AK(公开标识)+ SK(私密,只你侧保存、绝不上网/入库)。每次请求用 SK 对「方法+路径+参数+body+时间戳+nonce」算 HMAC-SHA256 签名,带 5 个头;服务端验签防篡改与重放。
  2. 用户身份(X-User-Token)——用户级隔离。先 POST /api/v1/users/register 拿 token,记忆读写带 X-User-Token: <token>;同一 user 跨会话共享,跨 user 物理隔离。register 本身也要签名(第 1 层)。

签名头(每个 /api/v1 请求都带)

头 值
X-Access-Key 你的 AK
X-Timestamp 当前 unix 秒(与服务端偏差需 ≤ 300s)
X-Nonce 每次请求唯一随机串(300s 内不可重复,防重放)
X-Signature HMAC-SHA256(SK, 规范串) 的 hex
X-Signed-Headers 实际纳入签名的头名(逗号分隔、小写、升序;无则空)

规范串(待签字符串,7 行 \n 连接,顺序固定)

<METHOD 大写>
<path,如 /api/v1/ingest>
<排序后 query,k=v 用 & 连;无则空行>
<应签头,每行 name:value(值去首尾空格);无则空行>
<body 的 SHA-256 hex(空 body 也算)>
<X-Timestamp>
<X-Nonce>

应签头最小集:content-type、x-user-token —— 请求带了(非空)就必须纳入签名(否则 401)。

可复制的签名示例(bash + openssl)

BASE=http://localhost:8000     # 换成对应环境
AK=rokid; SK='<你的SK>'; PATH_=/api/v1/health
TS=$(date +%s); NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '' | openssl dgst -sha256 -hex | awk '{print $NF}')   # GET 空 body
CANON=$(printf 'GET\n%s\n%s\n%s\n%s\n%s\n%s' "$PATH_" "" "" "$BODY_HASH" "$TS" "$NONCE")
SIG=$(printf '%s' "$CANON" | openssl dgst -sha256 -hmac "$SK" -hex | awk '{print $NF}')
curl -s "$BASE$PATH_" -H "X-Access-Key: $AK" -H "X-Timestamp: $TS" \
  -H "X-Nonce: $NONCE" -H "X-Signature: $SIG" -H "X-Signed-Headers: "

带 body/用户 token 的完整示例(POST /recall)、Python 版签名函数、常见失败排查,见 AK/SK 验签接入。未签名/签名错/超时/重放一律返回 401 {"code":401,"msg":"认证失败"}。探针 /healthz、/readyz 与本文档页不需签名。

统一响应信封

所有 /api/v1 接口的响应体统一为 {code, data, msg}:

字段 含义
code 业务码。0 = 成功;非 0 = 失败(取值 = HTTP 状态号,见下表)
data 成功时的业务载荷;失败时为 null
msg "ok" 或人可读的失败原因

HTTP 状态码保留 REST 语义(可用于网关/重试判断),与 code 同步:

HTTP / code 含义 处理建议
200 / 0 成功 —
201 / 0 创建成功(注册) —
202 / 0 已受理(异步写入已入队) 无需等待,继续
400 请求参数不合法 修正参数
401 token 缺失或无效 重新注册 / 检查请求头
404 资源不存在 —
413 图片过大(> 5MB) 压缩后重传
422 请求体结构校验失败 按 msg 修正字段
429 上游模型限流 按 Retry-After 退避重试
503 背压 / 繁忙(队列积压或并发已满) 按 Retry-After 降速重试
502 上游模型/向量服务故障 稍后重试

平台探针 GET /healthz、GET /readyz 在 /api/v1 之外,不套信封(仅供 k8s 探活)。

调用示意图

①  注册        POST /users/register         → 拿 token(一次性,持久保存)
                     │
②  会话开始    GET  /profile                → 拉用户画像,拼进系统提示词
                     │
       ┌─────────────┴──────────────┐   每轮对话循环
       │                            │
③  检索作答    POST /recall          │      → 拿记忆作答依据,Agent 组织回复
       │                            │
④  存储        POST /ingest ────────>┘      → 存本轮 QA 对(异步,不阻塞用户)
                     │
⑤  会话结束    POST /session/end            → 闭合尚未收口的段落(必调)
                     │
⑥  后台异步:队列消费 → 建记忆 → 画像整理     (无需调用;可用 /queue/status 查进度)

接入回路(推荐):

  1. 一次性:POST /users/register 拿 token 并持久保存。
  2. 会话开始:GET /profile 拉一次用户画像,拼进你的系统提示词。
  3. 每轮对话:先 POST /recall(拿记忆作答依据,你的 Agent 组织回复)→ 再 POST /ingest(把这轮的 QA 对存进记忆,异步不阻塞用户)。
  4. 会话结束:POST /session/end 闭合尚未收口的段落(必调)。
  5. 写入是异步的:/ingest、/session/end 入队即返回 202,后台按会话顺序消费、建记忆、并触发画像整理。可用 GET /queue/status 查消费进度。

快速开始(复制即跑)

⚠️ 下文各 curl 为可读性省略了签名头。实际所有 /api/v1 请求都要带 X-Access-Key/X-Timestamp/X-Nonce/X-Signature/X-Signed-Headers(见上方「鉴权(两层)」一节),否则返回 401。生产接入建议用 AK/SK 验签接入 里的签名函数包一层再发。

先注册拿 token 存进环境变量,后续所有命令直接复用:

export BASE=http://localhost:8000/api/v1

# 注册并把 token 存进 $TOKEN
export TOKEN=$(curl -s $BASE/users/register \
  -H 'Content-Type: application/json' \
  -d '{"user_id": "my_app_user_001"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")

echo "TOKEN=$TOKEN"

下文各接口的 curl 均假设已执行上面两行($BASE、$TOKEN 已就绪)。


POST /users/register — 注册用户

签发唯一 token 代表 user 身份。支持两种用法:传 user_id 用你自己的 id 注册;不传则系统自动生成一个。token 只在此响应完整返回一次,请持久保存,后续所有读写都用它。免鉴权。

请求体

字段 类型 必填 说明
user_id string 否 你的用户 id(仅字母/数字/下划线/连字符,≤128)。不传则自动生成

curl —— 用自定义 user_id 注册

curl -s http://localhost:8000/api/v1/users/register \
  -H 'Content-Type: application/json' \
  -d '{"user_id": "my_app_user_001"}'

响应 201

{
  "code": 0,
  "data": {
    "user_id": "doccap_1789461393",
    "token": "KjVUV7nb35JpiH7lrfoP3R_eE-HVYeE9"
  },
  "msg": "ok"
}

curl —— 不传 user_id,系统自动生成

curl -s http://localhost:8000/api/v1/users/register \
  -H 'Content-Type: application/json' -d '{}'
// 201
{
  "code": 0,
  "data": {
    "user_id": "u_01M2J3BWWZRQN7290MNVK81E27",
    "token": "3uh80oI7Ccg3akKGkT-zAHw4CbQ8bR-a"
  },
  "msg": "ok"
}

user_id 已存在 → 409;字符非法 → 400。


POST /ingest — 喂一批消息(异步)

把对话存进记忆。建议一次只传一个 QA 对(一问一答),这样话题边界最干净、抽取质量最好。入队即返回 202,后台按会话 FIFO 顺序消费,你无需等上一批完成再发下一批。

请求体

字段 类型 必填 说明
session_id string 是 会话 id(仅字母/数字/下划线/连字符/点,≤95,禁冒号)
caller string 否 调用方标识;非空时服务端把会话隔离为 caller:session_id。recall/session_end 须传一致的 caller
messages array 是 一批消息(1–20 条)。建议就放一个 QA 对。批内是原子:整批一起进同一段落,一对 QA 永不被切开
messages[].speaker string 是 说话人。与助手对话的本人固定用 "user";多人对话用对话里的名字
messages[].text string 否 文本(图片消息可空)
messages[].image_b64 string 否 base64 图片(带 data:image/...;base64, 前缀或纯 base64)
messages[].video_url string 否 视频 clip 的 http(s) 地址(含你自己桶的预签名 URL)。见「视频 clip」
messages[].video_oss_key string 否 视频 clip 已在我方 OSS 时的快捷路径(与 video_url 二选一)
messages[].clip_index int 否 你自己的 clip 编号(仅作元数据;真实顺序由服务端按到达次序定)
messages[].duration_sec float 否 clip 时长;填了可在入口就拦下超长的,省一次无谓的处理
context object 否 业务方上下文,见下
context.scenario string 否 业务场景描述(自由文本),让记忆更贴合你的场景。见「scenario 规范」
context.task_type array 否 episode 分类候选词表(闭合段落时给情景记忆打类型标,供 /episodes 按类型筛)

curl —— 一个 QA 对

curl -s $BASE/ingest \
  -H "Content-Type: application/json" -H "X-User-Token: $TOKEN" \
  -d '{
    "caller": "fitness-app",
    "session_id": "demo-session-001",
    "messages": [
      {"speaker": "user", "text": "我最近开始练力量,每周一三五去健身房"},
      {"speaker": "assistant", "text": "很棒!主要练哪些动作?"}
    ],
    "context": {
      "scenario": "健身健康助手 App:用户记录训练、饮食与身体反馈,并咨询自己的习惯与目标。重点记住:训练项目与频率、饮食偏好与忌口、身体指标与目标;对话通常较短、口语化。",
      "task_type": ["training", "diet", "body"]
    }
  }'

响应 202

{
  "code": 0,
  "data": {
    "accepted": true,
    "msg_id": "815f52aede164c7285b7e2e3b8aee57e",
    "seq": 1,
    "queue_depth": 1,
    "trace_id": "7888d8a7f2ee4850a592a249756d30e8"
  },
  "msg": "ok"
}

视频 clip(多模态写入)

把一段录像喂进记忆。服务端会看懂画面与对话、认出里面的人(跨会话延续同一人),再把它变成和文本同构的记忆——之后用普通的 /recall 就能问"视频里发生了什么""他做了什么"。

怎么传

方式 字段 适用
外链(推荐) video_url 你的 clip 在自己的存储里。传任意 http(s) 可下载地址(含预签名 URL),不要求传到我们桶里
我方 key video_oss_key clip 已经在我方 OSS,跳过下载转存

一条消息 = 一个 clip。 一批可以放多个 clip(1–20),按你给的顺序处理。

约束(务必看)

约束 上限 超了会怎样
单 clip 时长 150 秒 400(填了 duration_sec 时入口就拒;否则消费时拒并留痕)
单 clip 体积 64 MB 400,提示「请降低码率或切成更短的 clip」
同一条消息混内容 — 视频不能和 text/image_b64 放在同一条消息 → 400
同一批混内容 — 一批要么全视频、要么全文本/图片 → 400

⚠️ 64MB 不是我们存不下,是上游模型服务拉不动。 剧本理解是把地址交给模型服务、由它自己下载整段视频的,它那边有下载窗口。实测 106MB(2 分钟 @7.3Mbps)必然超时失败,55MB 长期稳定。建议 720p / ≤2 分钟 / 码率压到 2Mbps 以内(约 30MB)。

时序:视频记忆在 session/end 之后才可召回

逐个 clip 处理时只在内部累积"这段录像里有谁、谁说了什么";必须调 POST /session/end,服务端才会做身份终审并把整段录像落成一条情景记忆。没调 session/end 的话,这段视频不会进记忆(会话草稿 24 小时后自然作废)。

处理耗时(参考)

SIT 实测:60 秒的 clip 约 90 秒处理完(约 1.5× 实时),3 个 clip + 会话收尾约 405 秒。同一会话内的 clip 是串行的,所以单个会话跟不上实时视频流;不同会话之间并行。用 GET /queue/status 看消费进度,别靠猜。

curl

# 1) 灌 clip(可一次多个;这里用外链)
curl -s $BASE/ingest \
  -H "Content-Type: application/json" -H "X-User-Token: $TOKEN" \
  -d '{
    "caller": "glasses-app",
    "session_id": "living-room-0919",
    "messages": [
      {"speaker": "user", "video_url": "https://your-bucket/clip000.mp4?sign=...", "clip_index": 0},
      {"speaker": "user", "video_url": "https://your-bucket/clip001.mp4?sign=...", "clip_index": 1}
    ]
  }'

# 2) 收尾(必须):身份终审 + 落记忆
curl -s $BASE/session/end \
  -H "Content-Type: application/json" -H "X-User-Token: $TOKEN" \
  -d '{"caller":"glasses-app","session_id":"living-room-0919"}'

失败不静默:某个 clip 因地址失效/超大/超长被拒时,整批其余 clip 继续处理,被拒的那条会留一条 task 记录(kind=video_clip_rejected),可凭它查到"哪条 clip 没进记忆"。

callerContext / scenario 规范

scenario 是说给记忆引擎听的一段业务背景,它会注入切段、抽取事实、召回、作答、画像整理等环节,让记忆更贴合你的场景。铁律:它只调「关注度 / 详略」,不改事实、不编造、不遗漏——最坏情况是没效果,不会让记忆失真。建议覆盖四要素:

  1. 业务是什么:一句话点明 App/Agent 定位(如「健身健康助手」)。
  2. 重点记什么:你最在意被记住/召回的领域(如「训练项目与频率、饮食忌口、身体指标」)。
  3. 对话形态(可选):短口语 / 长而信息密集 / 有无频繁打断——影响切段松紧。
  4. 弱化什么(可选):此场景基本不用记的内容。

✅ Good:「饮食健康 App:用户记录三餐并咨询饮食习惯。重点记住饮食偏好与忌口、每餐时间与热量目标;对话较短、口语化。与饮食无关的闲聊可弱化。」——业务清晰、重点领域具体、点明形态。

❌ Bad:「重要的都记下来」(空泛无重点)/「只记饮食,其它全忽略」(scenario 不是过滤器,不会丢事实)/「没提到就默认过敏花生」(要求编造,不会照做)/ 把用户档案塞进来(那是画像,靠对话自然沉淀)。

建议 ingest 与 recall 用同一段 scenario,写入侧重与召回侧重才对齐。task_type 召回时忽略。

背压与限流(务必处理)

场景 响应 处理
单会话积压 > 15 条 503 + Retry-After 降速,稍后重发
入队争用繁忙 503 + Retry-After 同上
图片 > 5MB 413 压缩后重传
消息数不在 1–20 400 修正
视频 clip > 64MB 400 降码率或切更短的 clip
视频 clip 时长 > 150s 400 切成更短的 clip
视频地址不可访问(4xx/5xx) 400 检查地址是否失效/签名是否过期
视频与文本/图片混在同一条消息或同一批 400 拆成两条/两批分别发送

背压响应体(真实格式):

// 503   Header: Retry-After: 1
{ "code": 503, "data": null, "msg": "该会话消息积压过多,请降速后重试" }

POST /session/end — 会话收尾(异步)

会话结束时必须调用。它闭合当前会话中尚未收口的段落——未闭合的段落不会形成记忆(靠 24h 兜底作废),不调则会话尾部的对话可能进不了长期记忆。走同一条有序队列,排在该会话所有消息之后消费。

🎬 灌过视频的会话尤其不能漏:逐个 clip 处理时只在内部累积"这段录像里有谁、谁说了什么",身份终审与记忆落库都发生在这一步。不调 session/end 的话整段录像都不会进记忆(会话草稿 24 小时后作废)。这一步耗时也更长(要复核每个人物、再把整段录像织成一条情景记忆),用 GET /queue/status 等它消费完再去召回。

请求体:session_id(必填)、caller(与 ingest 一致)、context(可选,同 ingest)。

curl

curl -s $BASE/session/end \
  -H "Content-Type: application/json" -H "X-User-Token: $TOKEN" \
  -d '{
    "caller": "fitness-app",
    "session_id": "demo-session-001",
    "context": {"task_type": ["training", "diet", "body"]}
  }'

响应 202

{
  "code": 0,
  "data": {
    "accepted": true,
    "msg_id": "8cd1cb7f330e44a29f8e67b9247a79cd",
    "seq": 4,
    "session_id": "demo-session-001",
    "trace_id": "d12a60ca289f49d0ab948d82d9ebb036"
  },
  "msg": "ok"
}

GET /queue/status — 查会话消费进度

写入是异步的,用此接口确认「我发的消息消费到哪了」。cursor = 已消费到的最大 seq;depth = 待消费积压。

curl

curl -s "$BASE/queue/status?caller=fitness-app&session_id=demo-session-001" \
  -H "X-User-Token: $TOKEN"

响应 200

{
  "code": 0,
  "data": { "session_id": "demo-session-001", "depth": 0, "cursor": 4 },
  "msg": "ok"
}

POST /recall — 检索 + 作答(同步)

按问题从记忆里检索,返回一段标准事实陈述(第三人称、中立、带绝对日期)作为作答依据,以及支撑它的记忆条目(每条内联原始证据可溯源)。answer 是事实输出,由你的 Agent 组织成最终对话。

请求体

字段 类型 必填 说明
session_id string 是 与写入一致(用于补全指代等会话上下文)
caller string 否 与写入一致
query string 是 用户问题
image_b64 string 否 随问题带一张图(带 data:image/...;base64, 前缀或纯 base64)。见「带图提问」
image_content_type string 否 图片 MIME,默认 image/jpeg
mode string 否 fast / auto / deep,默认 auto。见下
top_k int 否 检索池上限,默认 30,一般不调
context.scenario string 否 同 ingest,调优召回方向与作答详略(只调侧重,不改事实)

带图提问(多模态召回)

用户发一张照片问"他和我上周干嘛去了?"——纯文字链路既看不到图,也不知道"他"是谁。传了 image_b64 后,服务端会在检索之前先看图:从图里检测人脸、和你这个 user 已知的人物比对,再结合会话上下文把问题补全成能检索的形式:

"他和我上周干嘛去了?"   ──[图 + 已知人物 + 会话上下文]──▶   "李四和我上周干嘛去了?"

补全后的问题再走正常的检索作答流程,所以你拿到的响应结构完全不变。

项 说明
数量 一次一张。多模态召回目前只支持文字 + 图片,不支持视频
体积 ≤ 5MB;超了 413,坏 base64 400
格式 jpeg / png / webp / gif / heic(按魔数识别,不认声明);认不出 → 400。查询图比 ingest 的图片更严:它是查询输入,认不出格式则整个视觉理解无从谈起,与其回 200 + 一个没用上图的答案,不如当场告诉你
认不出人 不会瞎编名字,改用外观描述("照片里那个穿蓝夹克的男生"),检索照常
图里没人 照样看图:地点、招牌文字、物体也能把"这家店""这个东西"落成具体词
看图失败 绝不阻塞召回——解码失败/模型不可用时按原问题检索,只是少一层理解
不传图 链路与纯文本召回逐字节一致,无额外开销

前提:要认出人,得先有这个人的记忆——即之前灌过包含他的视频(见 ingest 的「视频 clip」)。新用户库里没人时,只会用场景信息改写,不会认人。

怎么知道图被读懂了没 —— 带图时响应会多一个 visual 块:

"visual": {
  "faces": 1,
  "matched": [{"character_id": "char_01M2YPYP9SAZS78X5F4DX7B1G8", "name": "Bob"}],
  "resolved_query": "Bob 做了什么惹人生气的事?"
}
字段 含义 你可以据此做什么
faces 图里检测到几张人脸 0 → 提示用户「照片里没看到人,要不要换一张?」
matched 认出来的人(库里的 character_id + 名字);认不出则为 [] 有人 → UI 上可显示「关于 Bob 的记忆」;faces>0 但为空 → 「这个人我还不认识」
resolved_query 问题被改写成了什么(与原问题相同 = 图没能补全任何指代) 展示给用户看系统怎么理解他的问题

没有它你分不清两件事:「认出了人但记忆里确实没有」和「根本没认出人,按原问题检索」—— 两者答案都可能是"材料中没有相关信息",但前者该让用户换个问法,后者该让用户换张清楚的照片。

不带图时不会出现 visual 块(纯增量,老调用方不受影响)。

curl

curl -s $BASE/recall \
  -H "Content-Type: application/json" -H "X-User-Token: $TOKEN" \
  -d '{
    "caller": "glasses-app",
    "session_id": "living-room-0919",
    "query": "他做了什么惹人生气的事?",
    "mode": "fast",
    "image_b64": "'"$(base64 -i ./photo.jpg | tr -d '\n')"'"
  }'

三种模式

mode 说明 时延(参考)
fast 只走快链(检索→作答→核判),快而稳 ~2–6s
auto 快链为主,核判不过时自动升级到深轨多步翻阅 ~4–20s
deep 直接走深轨 agent 多步检索,最全但最慢 ~15–20s

💡 建议先只用 fast 模式:绝大多数问题快链已能答好,时延最低、成本最省。待发现某类问题快链答不全,再对该类问题改用 auto(让它按需自动升深轨)。

curl(fast)

curl -s $BASE/recall \
  -H "Content-Type: application/json" -H "X-User-Token: $TOKEN" \
  -d '{
    "caller": "fitness-app",
    "session_id": "demo-session-001",
    "query": "我健身练什么?教练是谁?",
    "mode": "fast"
  }'

响应 200(memories 共 9 条,此处示前 2 条)

{
  "code": 0,
  "data": {
    "query": "我健身练什么?教练是谁?",
    "mode": "fast",
    "verdict": "ok",
    "critique": "",
    "retried": false,
    "answer": "用户每周一、周三、周五去健身房进行力量训练,主要训练动作为深蹲、硬拉和卧推。教练是Mike。这些信息均记录于2026年9月15日。",
    "cited_cells": ["cell_01M2J3C1Q82S35V2SFGE2K81ES"],
    "memories": [
      {
        "atom_id": "atom_01M2J3C8TE2K09Z9603YNRJE8K",
        "cell_id": "cell_01M2J3C1Q82S35V2SFGE2K81ES",
        "text": "用户的力量训练教练是Mike (as of 2026-09-15)",
        "type": "事实",
        "holder": "user",
        "domains": ["D05", "D07"],
        "kind": "K02",
        "occurred_at": "2026-09-15T16:36:34.534353+08:00",
        "evidence_refs": ["ev_01M2J3BX3N4W8Z5YDETWDNEFX9"],
        "evidence": [
          { "id": "ev_01M2J3BX3N4W8Z5YDETWDNEFX9", "holder": "user",
            "content": "深蹲硬拉卧推,教练是 Mike", "captured_at": "2026-09-15T16:36:34.548811+08:00" }
        ]
      },
      {
        "atom_id": "atom_01M2J3C8TE2K09Z9603YNRJE8F",
        "cell_id": "cell_01M2J3C1Q82S35V2SFGE2K81ES",
        "text": "用户每周一、周三、周五去健身房练力量 (as of 2026-09-15)",
        "type": "事实",
        "holder": "user",
        "domains": ["D05", "D11"],
        "kind": "K05",
        "occurred_at": "2026-09-15T16:36:34.534353+08:00",
        "evidence_refs": ["ev_01M2J3BX3632D261ZFDT6YC65B"],
        "evidence": [
          { "id": "ev_01M2J3BX3632D261ZFDT6YC65B", "holder": "user",
            "content": "我最近开始练力量,每周一三五去健身房", "captured_at": "2026-09-15T16:36:34.534353+08:00" }
        ]
      }
    ],
    "trace_id": "6e6c51c52d744943b14049385be9d49a"
  },
  "msg": "ok"
}

响应字段

字段 说明
answer 事实陈述(空串 = 无相关记忆,此时会客观交代查了什么/为何答不出)
verdict 核判结论:ok / answer_defect(重答后仍缺陷) / insufficient_material(材料不足)
critique 核判指正(可据此追问或换问法)
retried 是否重答过一次
cited_cells 作答引用的记忆段 id
memories 依据记忆条目;每条:atom_id/cell_id、text(事实文本)、type(事实/说法/经历)、holder(归属人)、domains+kind(内部分类编码,可忽略)、occurred_at(事实发生时间)、evidence(原始对话原话,溯源用)
trace_id 排障链路 id

mode: "auto" 只改 mode 字段即可;缺 token → 401;mode 非法 → 400;上游限流 → 429;上游故障 → 502。


GET /profile — 取用户画像

建议在会话开始时拉一次,把画像拼进你的系统提示词,让对话更懂用户。画像是服务端从历史对话自动整理的,分 traits(稳定画像)与 facts(近况,按时间分档)。调用方读字段自行组织成自然语言即可。

traits — 稳定画像(PMO-16 域,键名固定,每个域一句话;无对应对话的域不出现)

层 域键 含义
禀性 personality / communication_style / social_style 性格 / 沟通风格 / 社交风格
特征适应 occupation / goals / values 职业 / 目标 / 价值观
work_style / learning_style / tech_environment 工作方式 / 学习方式 / 技术环境
lifestyle / health / finance 生活方式 / 健康 / 财务
叙事身份 identity / location / family / interests 身份认同 / 常驻地 / 家庭 / 兴趣

每个 trait:text(一句话)、status(confirmed 用户明说 / inferred 推断)、last_confirmed(最近确认日期)。

facts — 近况(按时间分档,越近越靠前)

档 含义
today 今天的事(整合为 1 条)
week 近 7 天(≤3 条)
month 近 30 天(≤5 条)
long 持久事实(身份、长期偏好、稳定关系等,≤21 条)

curl

curl -s $BASE/profile -H "X-User-Token: $TOKEN"

响应 200

{
  "code": 0,
  "data": {
    "exists": true,
    "version": 1,
    "updated_at": "2026-09-15T16:36:59",
    "traits": {
      "health": {
        "text": "有乳糖不耐受,不能喝牛奶,平时喝燕麦奶作为替代饮品",
        "status": "confirmed", "last_confirmed": "2026-09-15"
      },
      "lifestyle": {
        "text": "每周一、周三、周五去健身房练力量,主要做深蹲、硬拉和卧推,教练是Mike",
        "status": "confirmed", "last_confirmed": "2026-09-15"
      }
    },
    "facts": {
      "today": [
        { "text": "2026年9月15日告知自己最近开始练力量,每周一三五去健身房做深蹲、硬拉和卧推,教练是Mike;并告知有乳糖不耐受,不能喝牛奶,平时喝燕麦奶",
          "last_confirmed": "2026-09-15" }
      ],
      "week": [], "month": [], "long": []
    }
  },
  "msg": "ok"
}

新用户尚无画像 → data.exists = false(非错误,200)。画像是异步整理的,ingest 后需要几秒到十几秒才更新。


GET /episodes — 查情景记忆(分页)

按 task_type + 时间等条件查询最近会话的情景记忆(每段一条叙事)。按会话时间倒序(最近在前),分页返回。适合做「最近聊了什么」的回顾。

Query 参数

参数 类型 必填 说明
episode_type string 否 按类型筛(取值来自你 ingest 时传的 task_type);不传=全部
start / end string(ISO) 否 按段落起始时间过滤(含端点)
page int 否 页码,默认 1
page_size int 否 每页条数,默认 20,上限 100

每条 episode:memcell_id、session_id、start_time/end_time、topic(一句话主题)、episode(段落叙事)、episode_type(分类)。

curl —— 全量(时间倒序)

curl -s "$BASE/episodes?page=1&page_size=20" -H "X-User-Token: $TOKEN"

curl —— 按类型过滤

curl -s "$BASE/episodes?episode_type=training" -H "X-User-Token: $TOKEN"

响应 200

{
  "code": 0,
  "data": {
    "items": [
      {
        "memcell_id": "cell_01M2J3CCPCEM9VVR57XYE260CM",
        "session_id": "fitness-app:demo-session-001",
        "start_time": "2026-09-15T16:36:35.659716+08:00",
        "end_time": "2026-09-15T16:36:35.659716+08:00",
        "topic": "用户告知自己有乳糖不耐受,不能喝牛奶,平时喝燕麦奶",
        "episode": "2026年9月15日16:36,用户告知助手自己有乳糖不耐受,不能喝牛奶,平时以燕麦奶作为替代饮品。",
        "episode_type": "diet"
      },
      {
        "memcell_id": "cell_01M2J3C1Q82S35V2SFGE2K81ES",
        "session_id": "fitness-app:demo-session-001",
        "start_time": "2026-09-15T16:36:34.534353+08:00",
        "end_time": "2026-09-15T16:36:34.548811+08:00",
        "topic": "用户每周一三五去健身房练深蹲、硬拉和卧推,教练是Mike",
        "episode": "2026年9月15日,用户告诉助手,自己最近开始练力量,每周一、周三、周五去健身房。主要训练动作为深蹲、硬拉和卧推,教练是Mike。",
        "episode_type": "training"
      }
    ],
    "total": 2, "page": 1, "page_size": 20
  },
  "msg": "ok"
}

GET /trace/:node_id — 溯源(双向)

按 id 溯源,只在本 user 记忆内。传 atom_id → 正向:记忆全文 + 归属 + 下钻到原始证据(当时的对话原话);传 evidence_id → 反向:证据原文 + 被哪些记忆引用。都查不到 → 404。

curl

curl -s $BASE/trace/atom_01M2J3C8TE2K09Z9603YNRJE8K -H "X-User-Token: $TOKEN"

响应 200

{
  "code": 0,
  "data": {
    "node": "memory",
    "atom_id": "atom_01M2J3C8TE2K09Z9603YNRJE8K",
    "text": "用户的力量训练教练是Mike (as of 2026-09-15)",
    "cell_id": "cell_01M2J3C1Q82S35V2SFGE2K81ES",
    "object_type": "fact", "holder": "user",
    "domains": ["D05", "D07"], "kind": "K02",
    "evidence": [
      { "id": "ev_01M2J3BX3N4W8Z5YDETWDNEFX9", "holder": "user",
        "content": "深蹲硬拉卧推,教练是 Mike", "captured_at": "2026-09-15T16:36:34.548811+08:00" }
    ],
    "evidence_count": 1
  },
  "msg": "ok"
}

附:鉴权失败示例

未带 X-User-Token 请求任意业务接口:

curl -s $BASE/recall -H "Content-Type: application/json" \
  -d '{"session_id": "demo-session-001", "query": "x"}'
// 401
{ "code": 401, "data": null, "msg": "X-User-Token 缺失或无效,请先 POST /api/v1/users/register" }