fix(memory,loop): 记忆作用域读写成对 + source_conversation 结构化(裁决 1.9/04.4.2/04.4.3)
- 04.4.2/1.9 读侧:Retrieve 候选集并集 = 本人 persona 域 ∪ user 共享域 ∪ project 共享域(他角色 persona 域仍排除),群聊/陌生人取受限收窄
- 写侧:群聊/陌生人整段跳过记忆提取(可查不可写),persona 域仅属主可写
- 04.4.3:source_conversation 不再由 LLM 自由生成(prompt 不索要、候选结构不含该键),落库时无条件盖真实 {session_id,message_id},编解码集中在 memory/source.go
- 新增跨角色种子用例(原用例即使关掉并集也会通过)+ 成对用例「同群聊上下文:读得到共享域、写不进 store」
- 同步 store.go/retrieve.go 文档注释,与 design/03 §3/§4 口径一致
This commit is contained in:
1 parent
af8591b78a
commit
0ce498754a
11 files changed
+428
-60
No files matched your search
@@ -42,8 +42,10 @@ type Result struct {
|
||||
Item MemoryItem
|
||||
Score float64
|
||||
// SourceConversation is the conversation the item was extracted from, for
|
||||
// 溯源 (design §2). It mirrors Item.SourceConversation so callers can trace a
|
||||
// hit without reaching into Item.
|
||||
// 溯源 (design §2). 裁决 04.4.3 起为结构化 JSON(session_id/message_id,
|
||||
// 见 ParseSourceConversation),由持久化层盖真实 id。It mirrors
|
||||
// Item.SourceConversation so callers can trace a hit without reaching into
|
||||
// Item.
|
||||
SourceConversation string
|
||||
// Project is the linked project-index entry (设计 §3 项目分层), populated only
|
||||
// when the retriever is wired with a project-index store and the item is a
|
||||
@@ -52,11 +54,13 @@ type Result struct {
|
||||
Project *projectindex.Project
|
||||
}
|
||||
|
||||
// Retriever ranks a persona's long-term memories by relevance (keyword and,
|
||||
// when an embedder or vector index is configured, semantic vectors) weighted by
|
||||
// decay (decay_weight × time decay), then returns the top K. It is stateless and
|
||||
// safe for concurrent use; the ranking is recomputed from the store on every
|
||||
// call, so results always reflect persisted state.
|
||||
// Retriever ranks the long-term memories visible to a persona — its own items
|
||||
// plus the shared user/project scopes (design §3/§4, see visibleCandidates) —
|
||||
// by relevance (keyword and, when an embedder or vector index is configured,
|
||||
// semantic vectors) weighted by decay (decay_weight × time decay), then returns
|
||||
// the top K. It is stateless and safe for concurrent use; the ranking is
|
||||
// recomputed from the store on every call, so results always reflect persisted
|
||||
// state.
|
||||
type Retriever struct {
|
||||
store *Store
|
||||
embedder embedding.Embedder
|
||||
@@ -149,16 +153,20 @@ func NewRetriever(store *Store, opts ...RetrieverOption) *Retriever {
|
||||
|
||||
// Retrieve returns up to topK items for personaID that best match query, ranked
|
||||
// by relevance (keyword + optional semantic) weighted by decay (decay_weight ×
|
||||
// time decay). Query and candidate contents are both read under ctx; a canceled
|
||||
// context aborts the work. A blank query returns the most recently used items
|
||||
// (decay order only).
|
||||
// time decay). 候选集是 design/03 §3/§4 的可见域并集:该角色自己的条目
|
||||
// (ListByPersona)∪ 全局共享的用户习惯域(ScopeUser)∪ 人人可读的
|
||||
// 项目域(ScopeProject),按 id 去重。他角色的角色日常记忆(ScopePersona)
|
||||
// 绝不进入候选集 —— 跨角色读只走「把 B 当子代理去查 B」的派发机制
|
||||
// (design/22 §9),直接检索不越权。Query and candidate contents are both
|
||||
// read under ctx; a canceled context aborts the work. A blank query returns the
|
||||
// most recently used items (decay order only).
|
||||
func (r *Retriever) Retrieve(ctx context.Context, personaID, query string, topK int) ([]Result, error) {
|
||||
if topK <= 0 {
|
||||
topK = defaultTopK
|
||||
}
|
||||
items, err := r.store.ListByPersona(ctx, personaID)
|
||||
items, err := r.visibleCandidates(ctx, personaID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("memory: retrieve list: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return []Result{}, nil
|
||||
@@ -214,6 +222,39 @@ func (r *Retriever) Retrieve(ctx context.Context, personaID, query string, topK
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// visibleCandidates 载入 personaID 可见的候选记忆并集(design/03
|
||||
// §3/§4):本人条目(ListByPersona)∪ ScopeUser 全局共享域 ∪
|
||||
// ScopeProject 人人可读域,按 id 去重。本人条目先入集,因此去重
|
||||
// 保留的是同一行的唯一实例;他角色的 ScopePersona 条目(角色
|
||||
// 日常)不在其列 —— 跨角色读走子代理派发(design/22 §9),
|
||||
// 直接检索不得越权。任一载入失败都整体报错,绝不静默丢弃
|
||||
// 某个共享域。
|
||||
func (r *Retriever) visibleCandidates(ctx context.Context, personaID string) ([]MemoryItem, error) {
|
||||
own, err := r.store.ListByPersona(ctx, personaID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("memory: retrieve list: %w", err)
|
||||
}
|
||||
seen := make(map[string]struct{}, len(own))
|
||||
for _, it := range own {
|
||||
seen[it.ID] = struct{}{}
|
||||
}
|
||||
items := own
|
||||
for _, scope := range []Scope{ScopeUser, ScopeProject} {
|
||||
shared, err := r.store.ListScope(ctx, scope, personaID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("memory: retrieve list %s scope: %w", scope, err)
|
||||
}
|
||||
for _, it := range shared {
|
||||
if _, dup := seen[it.ID]; dup {
|
||||
continue
|
||||
}
|
||||
seen[it.ID] = struct{}{}
|
||||
items = append(items, it)
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// keywordScores scores each item against query with an in-memory TF-IDF inverted
|
||||
// index (embedding.Index), reusing the embedding package's tokenizer. Scores are
|
||||
// normalized to [0,1] by the maximum so they are comparable with the semantic
|
||||
|
||||
Reference in new issue
Block a user