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
@@ -12,15 +12,18 @@ import (
|
||||
|
||||
// extractSystemPrompt tells the model.Adapter how to produce candidate memories:
|
||||
// a JSON array of objects, one per memory worth persisting. Format is locked so
|
||||
// parseCandidates can unmarshal it deterministically.
|
||||
// parseCandidates can unmarshal it deterministically. 溯源(source_conversation)
|
||||
// 刻意不在其中:它由持久化层盖真实 session/message id,绝不由模型生成
|
||||
// (裁决 04.4.3,见 source.go)。
|
||||
const extractSystemPrompt = `你是记忆提取器。从对话中提取值得长期记住的记忆项(用户偏好、习惯、约定、重要事件)。
|
||||
只输出一个 JSON 数组,不要任何其他文字。每个元素形如:
|
||||
{"content":"记忆内容","scope":"user|persona|project","kind":"relationship|world_event","tags":["标签"],"confidence":0.9,"project_id":"可选项目id","source_conversation":"溯源"}
|
||||
{"content":"记忆内容","scope":"user|persona|project","kind":"relationship|world_event","tags":["标签"],"confidence":0.9,"project_id":"可选项目id"}
|
||||
去噪去重:只保留值得长期记住的,忽略寒暄与临时信息。`
|
||||
|
||||
// llmCandidate is the wire shape the extractor asks the LLM to emit: one JSON
|
||||
// object per candidate memory. Scope/Kind default to the extractor's configured
|
||||
// values when the model does not provide a valid one.
|
||||
// values when the model does not provide a valid one. 不含 source_conversation:
|
||||
// 模型即使回填该键也会被忽略(结构化溯源只来自持久化层)。
|
||||
type llmCandidate struct {
|
||||
Content string `json:"content"`
|
||||
Scope string `json:"scope"`
|
||||
@@ -28,7 +31,6 @@ type llmCandidate struct {
|
||||
Tags []string `json:"tags"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Source string `json:"source_conversation"`
|
||||
}
|
||||
|
||||
// LLMExtractor implements Extractor by asking a model.Adapter to derive candidate
|
||||
@@ -180,7 +182,8 @@ func parseCandidates(s string) ([]llmCandidate, error) {
|
||||
// the turn's role (the loop) stamps the persona when it persists the item — and
|
||||
// rejecting the item here would silently drop every memory of a multi-role
|
||||
// process. Content/scope/kind, which only this package can judge, are still
|
||||
// enforced.
|
||||
// enforced. SourceConversation 同理刻意留空:由提取调用点(持久化层)
|
||||
// 盖真实 session/message id(裁决 04.4.3,见 source.go)。
|
||||
func (e *LLMExtractor) dedupeAndBuild(cands []llmCandidate) []MemoryItem {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -196,15 +199,14 @@ func (e *LLMExtractor) dedupeAndBuild(cands []llmCandidate) []MemoryItem {
|
||||
continue
|
||||
}
|
||||
it := MemoryItem{
|
||||
PersonaID: e.personaID,
|
||||
RoleID: e.roleID,
|
||||
ProjectID: strings.TrimSpace(c.ProjectID),
|
||||
Scope: Scope(normalizeScope(c.Scope, string(e.scope))),
|
||||
Kind: Kind(normalizeKind(c.Kind, string(e.kind))),
|
||||
Content: content,
|
||||
Tags: c.Tags,
|
||||
Confidence: c.Confidence,
|
||||
SourceConversation: strings.TrimSpace(c.Source),
|
||||
PersonaID: e.personaID,
|
||||
RoleID: e.roleID,
|
||||
ProjectID: strings.TrimSpace(c.ProjectID),
|
||||
Scope: Scope(normalizeScope(c.Scope, string(e.scope))),
|
||||
Kind: Kind(normalizeKind(c.Kind, string(e.kind))),
|
||||
Content: content,
|
||||
Tags: c.Tags,
|
||||
Confidence: c.Confidence,
|
||||
}
|
||||
if !it.Scope.valid() || !it.Kind.valid() {
|
||||
continue
|
||||
|
||||
Reference in new issue
Block a user