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)。
鉴权(两层)
调用本服务有两层身份,叠加使用:
- 调用方签名(AK/SK)——服务级鉴权,所有
/api/v1/*请求必带。你会拿到一对AK(公开标识)+SK(私密,只你侧保存、绝不上网/入库)。每次请求用 SK 对「方法+路径+参数+body+时间戳+nonce」算 HMAC-SHA256 签名,带 5 个头;服务端验签防篡改与重放。 - 用户身份(
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 查进度)
接入回路(推荐):
- 一次性:
POST /users/register拿token并持久保存。 - 会话开始:
GET /profile拉一次用户画像,拼进你的系统提示词。 - 每轮对话:先
POST /recall(拿记忆作答依据,你的 Agent 组织回复)→ 再POST /ingest(把这轮的 QA 对存进记忆,异步不阻塞用户)。 - 会话结束:
POST /session/end闭合尚未收口的段落(必调)。 - 写入是异步的:
/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"
}
seq:该消息在会话队列里的序号;queue_depth:当前积压条数;trace_id:排障用的链路 id(有问题把它给我们即可定位)。
视频 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 是说给记忆引擎听的一段业务背景,它会注入切段、抽取事实、召回、作答、画像整理等环节,让记忆更贴合你的场景。铁律:它只调「关注度 / 详略」,不改事实、不编造、不遗漏——最坏情况是没效果,不会让记忆失真。建议覆盖四要素:
- 业务是什么:一句话点明 App/Agent 定位(如「健身健康助手」)。
- 重点记什么:你最在意被记住/召回的领域(如「训练项目与频率、饮食忌口、身体指标」)。
- 对话形态(可选):短口语 / 长而信息密集 / 有无频繁打断——影响切段松紧。
- 弱化什么(可选):此场景基本不用记的内容。
✅ 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" }