Skip to content

Commit 206a0bc

Browse files
author
linyuan.yang
committed
优化记忆提取
1 parent 4ab21c0 commit 206a0bc

20 files changed

Lines changed: 508 additions & 186 deletions

File tree

packages/admin/src/i18n/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,8 @@ export default {
533533
name_placeholder: 'e.g. Default Memory',
534534
writer_model: 'Writer Model',
535535
writer_model_placeholder: 'MemoryLLM (reasoning-capable recommended)',
536+
selector_model: 'Selector Model',
537+
use_writer_model: 'Use Writer Model',
536538
writer_prompt: 'Writer System Prompt',
537539
read_prompt: 'Read Path Template',
538540
run_consolidate: 'Clean Up Memories',

packages/admin/src/i18n/zh.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,8 @@ export default {
533533
name_placeholder: '如:默认长期记忆',
534534
writer_model: 'Writer 模型',
535535
writer_model_placeholder: '后台 MemoryLLM(建议 reasoning 强的模型)',
536+
selector_model: 'Selector 模型',
537+
use_writer_model: '使用 Writer 模型',
536538
writer_prompt: 'Writer 系统 Prompt',
537539
read_prompt: 'Read 路径模板',
538540
run_consolidate: '整理记忆',

packages/admin/src/views/automation/MemoryProfilesView.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface MemoryProfileForm {
1212
name: string
1313
enabled: boolean
1414
writerModel: string
15+
selectorModel: string
1516
writerPromptFile: string
1617
readPromptFile: string
1718
}
@@ -42,6 +43,7 @@ const columns = computed<STableColumn[]>(() => [
4243
{ key: 'name', label: t('common.name'), primary: true },
4344
{ key: 'enabled', label: t('common.enabled'), width: '100px', align: 'center' },
4445
{ key: 'writerModel', label: t('memory_profiles.writer_model'), width: '200px' },
46+
{ key: 'selectorModel', label: t('memory_profiles.selector_model'), width: '200px' },
4547
{ key: 'ops', label: t('common.ops'), ops: true, width: '320px', align: 'center' },
4648
])
4749
@@ -55,6 +57,7 @@ function emptyForm(): MemoryProfileForm {
5557
name: '',
5658
enabled: true,
5759
writerModel: '',
60+
selectorModel: '',
5861
writerPromptFile: '',
5962
readPromptFile: '',
6063
}
@@ -94,6 +97,7 @@ function openEdit(id: string) {
9497
name: p.name || '',
9598
enabled: !!p.enabled,
9699
writerModel: p.writerModel || '',
100+
selectorModel: p.selectorModel || '',
97101
writerPromptFile: p.writerPromptFile || '',
98102
readPromptFile: p.readPromptFile || '',
99103
}
@@ -112,6 +116,7 @@ async function save() {
112116
enabled: form.value.enabled,
113117
writerModel: form.value.writerModel,
114118
}
119+
if (form.value.selectorModel) body.selectorModel = form.value.selectorModel
115120
if (form.value.writerPromptFile) body.writerPromptFile = form.value.writerPromptFile
116121
if (form.value.readPromptFile) body.readPromptFile = form.value.readPromptFile
117122
const id = editingId.value
@@ -198,6 +203,7 @@ function modelLabel(id: string | undefined | null): string {
198203
</SBadge>
199204
</template>
200205
<template #writerModel="{ row }">{{ modelLabel(row.writerModel) }}</template>
206+
<template #selectorModel="{ row }">{{ row.selectorModel ? modelLabel(row.selectorModel) : t('memory_profiles.use_writer_model') }}</template>
201207
<template #ops="{ row }">
202208
<div class="ops-row">
203209
<SButton type="outline" size="sm" @click="openMemoryViewer(row.id)">{{ t('memory_profiles.view_memories') }}</SButton>
@@ -230,6 +236,12 @@ function modelLabel(id: string | undefined | null): string {
230236
<option v-for="m in modelOptions" :key="m.id" :value="m.id">{{ m.label }}</option>
231237
</SSelect>
232238
</SFormItem>
239+
<SFormItem :label="t('memory_profiles.selector_model')">
240+
<SSelect v-model="form.selectorModel">
241+
<option value="">{{ t('memory_profiles.use_writer_model') }}</option>
242+
<option v-for="m in modelOptions" :key="m.id" :value="m.id">{{ m.label }}</option>
243+
</SSelect>
244+
</SFormItem>
233245
<SFormItem :label="t('memory_profiles.writer_prompt')">
234246
<SSelect v-model="form.writerPromptFile">
235247
<option value="">{{ t('common.default') }}</option>

packages/admin/src/views/models/ModelsView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const refs = makeResourceRefs({
3232
profile: (p, id) => p.intentModel === id,
3333
session: (s, id) => s.intentModel === id,
3434
agent: (a, id) => a.model === id || a.compactModel === id,
35-
memoryProfile: (p, id) => p.writerModel === id,
35+
memoryProfile: (p, id) => p.writerModel === id || p.selectorModel === id,
3636
agendaProfile: (p, id) => p.syncModel === id,
3737
})
3838
const expandedIds = ref<string[]>([])

packages/docs-site/guide/channels.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,16 @@ Talk to your XiaoAi smart speaker as a sbot channel.
128128
| 登录方式 | `passToken` (recommended, obtained via **sbox**), or Mi account password |
129129
| 密码 / passToken | The credential matching the selected login mode |
130130
| 登录设备 ID | Optional PassportSDK `deviceId`, obtained via **sbox** |
131-
| 音箱名称 | Target speaker name (matches a device in your Mi account) |
131+
| 音箱名称 | Target speaker name (matches a device in your Mi account). One per line to bind several speakers to this channel |
132132

133133
2. The bot logs in to Mi Cloud, listens for voice messages on the device, and replies via TTS through the speaker
134134

135+
A single channel can drive multiple speakers — list them all in 音箱名称 rather than creating one channel per speaker. The speakers share one Mi Cloud login, and each gets its own session, so a reply always goes back to the speaker that was spoken to. Creating several channels for the *same* Mi account does not work: each login invalidates the previous `serviceToken`, so all but the last channel fail with HTTP 401.
136+
137+
::: warning
138+
Every speaker is polled independently, so N speakers means N × (1000 / 轮询间隔) requests per second against Mi Cloud. Raise 轮询间隔 when binding more than a few.
139+
:::
140+
135141
::: tip Getting `passToken` and `deviceId` with sbox
136142
Mi accounts often reject server-side password login (risk control / 2FA), so the recommended path is to log in once with **sbox** — a desktop helper toolbox — and copy the credentials it extracts:
137143

packages/docs-site/guide/memory.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22

33
Sidebar → **Memory Profiles** (under **Tasks**), then enable it per-agent in the agent edit page → **Memory**.
44

5-
Memory is the agent's automatic long-term memory. A background **MemoryLLM** reviews each conversation after it goes idle and distills durable knowledge — user preferences, project facts, decisions, lessons learned — into memory entries. On later turns the agent reads them back via the `search_memory` and `read_memory` tools.
5+
Memory is the agent's automatic long-term memory. After each conversation, a background Selector reviews the full memory catalog for relevant entries, then the **MemoryLLM** reads those entries' complete bodies and applies final changes. On later turns the agent reads memories back via the `search_memory` and `read_memory` tools.
66

77
Think of it as the agent learning from every conversation without you having to teach it explicitly — and without bloating the system prompt.
88

99
## How It Works
1010

11-
1. **Extract** — after a conversation idles, the **writer model** silently reviews the exchange and writes new memories (or updates/removes existing ones).
12-
2. **Read** — on subsequent turns the agent calls:
13-
- `search_memory` — fuzzy/keyword/semantic lookup across stored memories
11+
1. **Select** — after a conversation idles, the **selector model** matches the transcript against self-contained titles from every global + current-workspace entry. It uses one request while the title catalog fits its budget; for oversized catalogs it compresses the transcript once and matches titles in batches. The Selector never reads bodies. If no selector is configured, the writer model performs this step.
12+
2. **Extract** — the **writer model** reads the selected entries' complete bodies together with the conversation, then creates, updates, removes, or leaves memories unchanged.
13+
3. **Read** — on subsequent turns the agent calls:
14+
- `search_memory` — BM25 keyword lookup across stored memories
1415
- `read_memory` — fetch the full body of a memory by its slug
15-
3. **Maintain** — background jobs keep the store healthy:
16+
4. **Maintain** — background jobs keep the store healthy:
1617
- **Consolidate** — merges and de-duplicates related memories
1718
- **Reconcile** — re-indexes and prunes stale entries
18-
4. **Delete** — removed memories are moved to `.archive/` and can be recovered.
19+
5. **Delete** — removed memories are moved to `.archive/` and can be recovered.
1920

2021
## Global and Workspace Memory
2122

@@ -34,7 +35,8 @@ A **Memory Profile** defines how memories are extracted and read. Sidebar → **
3435
|-------|-------------|
3536
| Name | Display name for this profile |
3637
| Enabled | Pause the profile without deleting it |
37-
| Writer Model | The MemoryLLM used to extract memories (a reasoning-capable model recommended) |
38+
| Writer Model | The authoritative MemoryLLM that reads selected full bodies and applies final changes |
39+
| Selector Model | Optional cheaper model that screens the complete catalog; defaults to Writer Model |
3840
| Writer Prompt | Controls **what** gets extracted |
3941
| Read Prompt | How retrieved memories are formatted back into the prompt |
4042

packages/docs-site/zh/guide/channels.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,16 @@ OneBot 是 [NapCat](https://napneko.github.io/)、[Lagrange](https://lagrangedev
190190
| 登录方式 | `passToken`(推荐,通过 **sbox** 获取),或小米账号密码 |
191191
| 密码 / passToken | 与所选登录方式对应的凭据 |
192192
| 登录设备 ID | 可选,PassportSDK 的 `deviceId`,通过 **sbox** 获取 |
193-
| 设备名称 | 目标设备名(需与账号下的设备一致) |
193+
| 音箱名称 | 目标设备名(需与账号下的设备一致)。一行一台,可在本渠道同时绑定多台音箱 |
194194

195195
2. 机器人登录到小米云,监听设备上的语音消息,并通过音箱 TTS 回复
196196

197+
多台音箱请全部填进「音箱名称」,不要一台音箱建一个渠道。同一渠道下的音箱共用一次小米云登录,各自是独立会话,回复只会播回说话的那台。而给**同一个小米账号**建多个渠道是不可行的:每次登录都会作废上一份 `serviceToken`,除最后一个渠道外全都会 401 失败。
198+
199+
::: warning
200+
每台音箱各自独立轮询,N 台音箱即每秒 N × (1000 / 轮询间隔) 次小米云请求。绑定较多音箱时请相应调大「轮询间隔」。
201+
:::
202+
197203
::: tip 通过 sbox 获取 `passToken``deviceId`
198204
小米账号在服务器上常因风控 / 二次验证导致密码登录失败,推荐用桌面工具箱 **sbox** 登录一次,再把它提取出的凭据填入上面的字段:
199205

packages/docs-site/zh/guide/memory.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22

33
侧栏 → **Memory Profiles**(位于 **Tasks** 分组),再在 Agent 编辑页 → **Memory** 中按 Agent 开启。
44

5-
Memory 是 Agent 的自动长期记忆。后台 **MemoryLLM** 会在每次对话空闲后回顾对话,把持久价值的知识 —— 用户偏好、项目事实、决策、经验教训 —— 提炼成记忆条目。在后续对话中,Agent 通过 `search_memory``read_memory` 工具把它们读回来。
5+
Memory 是 Agent 的自动长期记忆。每轮对话结束后,后台 Selector 会先浏览完整记忆目录并筛出相关条目,再由 **MemoryLLM** 阅读候选正文并执行最终修改。在后续对话中,Agent 通过 `search_memory``read_memory` 工具把它们读回来。
66

77
可以把它理解为:Agent 从每次对话中自我学习,无需你显式教学,也不会撑爆系统提示词。
88

99
## 工作原理
1010

11-
1. **提取** —— 对话空闲后,**writer 模型** 静默回顾对话并写入新记忆(或更新 / 删除已有记忆)。
12-
2. **读取** —— 后续对话中 Agent 调用:
13-
- `search_memory` —— 在已存记忆中做模糊 / 关键词 / 语义查找
11+
1. **筛选** —— 对话结束后,**selector 模型** 用本轮对话匹配全部全局记忆和当前工作区记忆的自包含标题。标题在预算内时一次完成;目录过大时只将对话压缩一次,再用简短事实分批匹配标题。Selector 不读取正文;未配置时复用 writer 模型。
12+
2. **提取** —— **writer 模型** 阅读候选条目的完整正文和本轮对话,再决定新建、更新、删除或不修改。
13+
3. **读取** —— 后续对话中 Agent 调用:
14+
- `search_memory` —— 在已存记忆中做 BM25 关键词查找
1415
- `read_memory` —— 按 slug 读取某条记忆的完整内容
15-
3. **维护** —— 后台任务保持记忆库健康:
16+
4. **维护** —— 后台任务保持记忆库健康:
1617
- **Consolidate(合并)** —— 合并、去重相关记忆
1718
- **Reconcile(校正)** —— 重建索引、清理陈旧条目
18-
4. **删除** —— 被删除的记忆移到 `.archive/`,可恢复。
19+
5. **删除** —— 被删除的记忆移到 `.archive/`,可恢复。
1920

2021
## 全局与工作区记忆
2122

@@ -34,7 +35,8 @@ Memory 是 Agent 的自动长期记忆。后台 **MemoryLLM** 会在每次对话
3435
|-------|-------------|
3536
| 名称 | 该 Profile 的显示名称 |
3637
| 启用 | 暂停该 Profile,而不删除配置 |
37-
| Writer 模型 | 用于提取记忆的 MemoryLLM(推荐有推理能力的模型) |
38+
| Writer 模型 | 阅读候选完整正文并执行最终修改的权威 MemoryLLM |
39+
| Selector 模型 | 可选的低成本目录筛选模型;不设置时复用 Writer 模型 |
3840
| Writer 提示词 | 控制 **提取什么** |
3941
| Read 提示词 | 召回的记忆如何回填进提示词 |
4042

packages/sbot.commons/src/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ export interface MemoryProfileConfig {
9292
// ── 模型 ──
9393
/** MemoryLLM 模型 UUID(必填) */
9494
writerModel: string
95+
/** 候选筛选模型 UUID;不设置时复用 writerModel */
96+
selectorModel?: string
9597

9698
// ── prompt 文件(不设置则使用默认) ──
9799
/** MemoryWriter 系统提示路径,默认 'memory/writer/default.md' */
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
You are the low-cost analysis and candidate-selection stage for long-term memory.
2+
3+
You do not create, update, or delete memories. You only decide whether the conversation
4+
contains durable information worth reviewing and which title cards might overlap, be
5+
contradicted, or need their full bodies read by the authoritative Writer.
6+
7+
# Selection rules
8+
9+
- Weigh user messages far above assistant messages. User corrections, repeated
10+
preferences, explicit workflow rules, and durable environment facts are strong signals.
11+
- Ignore transient task state, one-off implementation details already obvious from code,
12+
unadopted brainstorming, and assistant-only claims.
13+
- In conversation-analysis modes, set `shouldWrite=true` when the transcript may justify
14+
a durable create, update, or delete. Express each durable fact as a short, self-contained
15+
statement suitable for matching against titles.
16+
- In catalog-selection modes, select an entry whenever its self-contained title might
17+
cover the same fact. Recall is more important than precision because the Writer reads
18+
candidate bodies and makes the final decision.
19+
- Candidates must copy the exact `scope` and `slug` shown in the supplied catalog.
20+
- Respect the requested candidate limit. In complete-catalog or conversation-analysis
21+
mode, use `shouldWrite=false` when nothing durable is present. In catalog-matching mode,
22+
simply return no candidates when no title matches.
23+
24+
Your output is structured data only. It grants no permission to mutate the memory store.

0 commit comments

Comments
 (0)