Files
homeagent-sdk/docs/guide/scene-memory.md
JianFeeeee bd73a9b241 feat(sdk): 通用反代声明项(DeclareProxy)+ ToolDef.Serial 串行标记 + 场面策略文档
本次一并提交工作区此前累积的改动(均已验证),并接入工具并发调度所需的
声明项。

把「谁来反代谁」从内核硬编码变成插件可声明。设备网关(remotedevice)
这类**编译进内核、没有独立插件目录与 plugin.json** 的服务,静态扫描扫不到,
此前只能靠约定。新增 DeclareProxy 让它们能自己声明反代路由。

ParallelSafe 的**反向**声明项。判据优先级:Serial 胜出,显式声明不允许被
ParallelSafe 或任何默认值覆盖。

为什么需要它:ParallelSafe 零值 false 已表达「安全/串行」,插件无法区分
「我没想过」和「我确认过必须串行」。没有这个区分,工具作者只能靠命名约定
传递意图,那不是契约。

ParallelSafe 本身也补齐了注释,明确其零值语义(默认串行、保守)与理由
(新语义下并发会改变工具的行为前提,让存量插件意外并发比慢一点危险得多)。

配套 ScenePolicy 声明项的使用说明。

remotedevice/ 整目录(C 实现的设备网关,已由 Go 侧 DeclareProxy 路径取代)。

- sdk/knowledge.go:随场面策略配套调整
- docs/api/*、docs/assets/api-index.json、docs/llms.txt、mkdocs.yml:
  由 tools/apidoc/build.sh 从源码重新生成(行号随 plugin.go 变动漂移)
2026-09-27 16:17:04 +08:00

114 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 场景记忆(Scene Memory)
> 场景式记忆是内核 v1.3 起的能力。它不新增 API 面,只影响**你的输入被怎样记住与取回**。
> 与 `NoMemory` / `ContextPolicy` / `RecallPolicy` 并列为第四项声明:`ScenePolicy`。
## 它解决什么问题
三层记忆按**字面相关性**召回:你得说出相近的词,记忆才会被取回来。
场景记忆补上另一半:按**场合**召回。
同一场合再次出现时,当时挂在这个场合上的约定、偏好、人物关系会自动回来——
与这次说了什么措辞无关。
```
你:以后在群里回消息简短点
└─ 这条记忆挂到场面「chan:qq + peer:group_xxx」上
一周后,同一个群里有人问「上次说的格式是什么」
└─ 场面重现(还没等你提到「格式」),那条约定已经被取回
```
## 场面是自己长出来的
场景**不需要声明**。每轮交互,内核采集一组可观察信号当这轮<E8BF99><E8BDAE><EFBFBD>「场面指纹」:
| 特征 | 来源 | 权重 | 说明 |
|---|---|---|---|
| `chan` | 输入通道名 | 1.0 | 最强的同一性信号 |
| `peer` / `peer_group` | 注入点给的 `payload` 里的 `group_id`/`user_id`/`chat_id` 等 | 1.0 | 群与私聊分开,避免互相命中 |
| `tool` | 触发这一步的工具名 | 0.8 | 行为信号 |
| `topic` | 清洗后输入的内容词 | 0.4 | 软信号,同场面的不同话题不该被拆开 |
| `part` | 时段(夜间/上午/下午/晚间) | 0.2 | 最弱,只做辅助 |
指纹反复重合时,一场场面就成形了。相似度按**加权 Jaccard** 算
(共享特征的权重和 ÷ 并集的权重和)——不加权的话,一次偶然的话题重合
会把两个不同场面并成一个。
**同类场面出现第二次才被认定。** 一次性的交互不建场面:
那不是「场面」,建了只会让图库被一次性事件撑满。
## 声明你的参与姿态
```go
sdk.ChannelDef{
ScenePolicy: sdk.ScenePolicyNone, // 这条通道不参与场面识别
}
```
或单次注入覆盖:
```go
sdk.InjectOptions{
ScenePolicy: sdk.ScenePolicyNone,
}
```
| 取值 | 含义 |
|---|---|
| `""`(空)/ `ScenePolicyAuto` | **参与**(默认,保持既有行为) |
| `ScenePolicyNone` | **不参与**:不产任何场面指纹,也不派生场景键 |
**默认是参与而不是不参与**,与 `ContextPolicy` 刻意相反。原因是场景只
**附加**检索路径、不改记忆本体,默认关会让存量通道突然失去场景召回;
而「关」是少数意图(纯内部信号)。
声明 `none` 之后连时段特征都不产——一个不参与的门面不该在场面索引里
留下任何足迹。
### 谁该考虑关掉
内核自循环(`system`)、心跳(`timer`)、内部状态汇报(`kernel`)这类
纯内部信号。它们每次触发都在撑一个场面,会把不相干的交互聚到一起。
反过来说,**多标一个通道通常没有代价**:一个没人往上面写记忆的场面,
召回时返回空。关不关都不影响正确性——所以拿不准时,默认参与就好。
## 怎么给场面命名
场景键有两种来源:
**通道派生(默认)**——`evt.Source` 派生出 `chan:qq` 这类键。你不用管。
**显式声明(进阶)**——在注入时给出更有语义的键:
```go
p.sdk.InjectInterruptTextOpts("qq", "qq", text, sdk.InjectOptions{
ScenePolicy: sdk.ScenePolicyAuto,
})
```
也可以通过 `payload["scene"]` 传层级键(支持 `string` / `[]string` /
`[]interface{}` 三种形态):
```go
"chan:qq/peer:group_1027"
```
召回走**前缀匹配**(`chan:qq` 能覆盖 `chan:qq/peer:xxx`),用 `/` 兜底
以免 `chan:qq` 误吞 `chan:qq2` 这种同前缀但不同层的场景。
## 场面记忆不改变什么
- **不改记忆本体**:场景是记忆的**附加索引**,删掉场景不删记忆。
- **不让模型负责**:`memory_commit` 的 `scene` 留空即可,内核会挂到本轮
解析出的场面上。留空是安全的一侧——猜错的场面会把无关记忆钉死。
- **不影响同步通道**:`webui` / `cli` / 终端走 `ResponseCh`,不经
`output_send__*`,与场面无关。
## 相关 API
- `ChannelDef.ScenePolicy` —— 通道级声明(见 [输入/输出通道](../api/channels.md))
- `InjectOptions.ScenePolicy` —— 单次注入覆盖(见 [其他类型](../api/misc.md))
- `ScenePolicyAuto` / `ScenePolicyNone` / `ValidScenePolicy` —— 常量与校验