mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
feat(memory): 场景式关联召回——给记忆节点赋场景引用,场面重现即取回
背景(实测):带条件的记忆召不回来。生产库里明明有 「QQ回复禁用Markdown格式 --规定--> 纯文本不用Markdown」「老大 --偏好--> 同左」, 但输入「QQ回复格式」时命中 148 个实体、规则排第 32,注入只取前 5——规则根本没进去; 输入「在吗」这种零内容词的短消息,向量路反而灌进 17 个毫不相关的实体。 根因:词法/向量召回都建立在「字面或语义相似」上,而条件式记忆(在什么场合该怎么做) 约束的是**场面**不是话题。用户措辞不重合时它天然召不回;措辞太宽("QQ")时又被同形 命中淹没。另一处:自动注入只给实体名索引,而规则本体长在关系上(relation_type + object), 即使命中名字也拿不到「纯文本不用Markdown」这句正文。 改动:把「触发条件」升成一等索引维度。 - schema:新增 scenes(key) + scene_refs(scene_id, kind, ref_id, weight), kind ∈ relation|entity。刻意不建外键:节点可能先于引用被清理, 悬空引用由读取侧 JOIN 过滤,级联删除会把清理变成跨表事务。 - 场景键是分层字符串(`/` 分隔,由宽到窄):chan:qq、chan:qq/peer:group_123、 tool:qq_get_message。NormalizeSceneKey 归一(小写、空白/标点→_、按 `/` 分层), 空白不算层级——否则「老大2026-09-04 12:27 QQ私聊图片」这种来源名会被拆成伪层级。 - 写入即挂场景:Triple 新增 Scene 字段,commit() 在同一事务里把「关系 + 两端实体」 挂到场景上(同事务是必须的:关系进库但引用丢了 = 这条记忆永远无声地召不回来)。 - 召回:RecallByScene 前缀匹配(chan:qq 取回 chan:qq 及所有更窄场景;用 `/` 兜底 防止 chan:qq 吞掉 chan:qq2),按 weight(=写入置信度)降序,返回**关系全文 + 原句**。 - 注入:BuildContextInScene 在词法/向量之外叠加场景路,FormatContext 把场景块排在 最前(规则对行为的约束强于话题相关的实体名),上限 8 条 + 原句截断 60 字; 场景实体不在【记忆索引】里重复占位。BuildContext(input) 保持原语义(无场景)。 - 当前场景推导:payload.scene 显式声明 > 通道(chan:qq)> 工具(tool:qq_get_message), 并列命中不取交集。qq 通道本身 RecallPolicy=none(到达的是中断元文本), 真正召回在 qq_get_message 工具上——现在那一步同时带上 chan:qq 与 tool:qq_get_message。 - 写入侧:memory_commit 新增 scene 参数(逐条 triples[].scene 优先,顶层 scene 作批次默认); docToTriples 按文档来源自动带 chan:<source>(QQ 归档的知识天然属于 QQ 场面)。 不做自动猜测:猜错的场景会把无关记忆钉死,之后每次进入该场面都被注入。 - 存量引导:memgc -tag-scene <键> -entity-glob <GLOB>。用 GLOB 而非 LIKE—— LIKE 对 ASCII 不区分大小写,`%QQ%` 会把对象带 /home/newqqagent 的路径类记忆 (生产数据目录、email-mcp、dify-ops 路径…实测 7 条)一起卷进 QQ 场景。 - 清理对齐:PurgeNoise/PurgeOrphans 之后顺带删悬空场景引用,并提供 PurgeStaleSceneRefs;memgc -scene-stats 看场景规模。 验证:go build/vet 干净,go test -count=1 ./... 全绿。 新增用例:场景键归一(含超长/分层/空白)、写入即挂场景(两端实体进、未标的实体不进)、 前缀语义(含 chan:qq2 反例)、weight 排序与 limit、GLOB 存量引导(dry-run 不写库)、 清理后无悬空引用、场景注入面(关系全文+原句+不在索引重复占位)、 agent 侧 sceneKeysFor 优先级(显式声明 > 通道 > 工具、数组形式、nil 安全)。 生产库实测(先 sqlite3 .backup 到 graph.db.bak-20260915-081043 再写): 把 22 条 QQ 相关关系标进 chan:qq(GLOB *QQ* 19 条 + *qq_* 3 条)。同一批输入前后对比: - 「在吗」:改前注入 17 个无关实体;改后场景块直接给出「QQ回复禁用Markdown格式 --规定--> 纯文本不用Markdown」等规则正文(零字面重合也能召回)。 - 「QQ回复格式」:改前规则排第 32 被截掉;改后排在场景块首位。 - 「帮我发个语音」:场景规则置顶,词法路的 qq通道语音输入 等仍在其后。
This commit is contained in:
@ -423,6 +423,11 @@ func docToTriples(doc *document.Doc, embedder nlp.Vectorizer) []memory.Triple {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 文档归档的知识是有**来源场面**的:来自 QQ 的对话归档,其三元组就该
|
||||
// 钉在 chan:qq 上。这样「又来一条 QQ 消息」时,这批知识靠场景就能取回,
|
||||
// 不必指望本轮措辞与它们字面重合。
|
||||
docScene := memory.ChannelScene(doc.Source)
|
||||
|
||||
isArchivedContext := doc.Meta != nil && doc.Meta["is_archived_context"] == "true"
|
||||
|
||||
// 文档元数据:仅当 summary 合理(非空、非模板化、长度适中)时才写「主题」
|
||||
@ -434,6 +439,7 @@ func docToTriples(doc *document.Doc, embedder nlp.Vectorizer) []memory.Triple {
|
||||
Object: doc.Summary,
|
||||
ObjectType: "Topic",
|
||||
Confidence: 1.0,
|
||||
Scene: docScene,
|
||||
})
|
||||
}
|
||||
|
||||
@ -451,6 +457,7 @@ func docToTriples(doc *document.Doc, embedder nlp.Vectorizer) []memory.Triple {
|
||||
for _, nt := range result.Triples {
|
||||
mt := nlp.ToMemoryTriple(nt)
|
||||
if mt.Subject != "" && mt.Relation != "" && mt.Object != "" {
|
||||
mt.Scene = docScene
|
||||
triples = append(triples, mt)
|
||||
}
|
||||
}
|
||||
@ -465,6 +472,7 @@ func docToTriples(doc *document.Doc, embedder nlp.Vectorizer) []memory.Triple {
|
||||
Object: doc.Source,
|
||||
ObjectType: "Source",
|
||||
Confidence: 1.0,
|
||||
Scene: docScene,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -379,7 +379,7 @@ func (a *Agent) pruneOnInput(evt *agentIO.InputEvent, cleanInput string) int {
|
||||
if !a.pruneDeclared(evt) {
|
||||
return 0
|
||||
}
|
||||
return a.memoryPass(cleanInput, "input:"+evt.Source, true, false).Archived
|
||||
return a.memoryPass(cleanInput, "input:"+evt.Source, true, false, sceneKeysFor(evt, "")).Archived
|
||||
}
|
||||
|
||||
// pruneDeclared 判定这次输入是否显式声明了裁剪。
|
||||
|
||||
@ -376,7 +376,7 @@ func TestBuildMemoryContext_IncludesMediaSection(t *testing.T) {
|
||||
t.Fatalf("indexer sync: %v", err)
|
||||
}
|
||||
|
||||
out := a.buildMemoryContext("测试图片", 0)
|
||||
out := a.buildMemoryContext("测试图片", 0, nil)
|
||||
if out == "" {
|
||||
t.Skip("图库召回未命中(indexer 检索策略所致),无法验证媒体段注入")
|
||||
}
|
||||
|
||||
@ -440,7 +440,7 @@ func TestMediaLive_AutoTriggerChain(t *testing.T) {
|
||||
if err := a.indexer.Sync(); err != nil {
|
||||
t.Fatalf("indexer sync: %v", err)
|
||||
}
|
||||
if mc := a.buildMemoryContext("测试图片", 0); mc != "" {
|
||||
if mc := a.buildMemoryContext("测试图片", 0, nil); mc != "" {
|
||||
t.Logf("注入的记忆上下文: %s", truncRunes(mc, 200))
|
||||
} else {
|
||||
t.Log("图库召回为空(本测试不再依赖文本描述,仅记录现状)")
|
||||
|
||||
@ -1,6 +1,61 @@
|
||||
package core
|
||||
|
||||
import "log"
|
||||
import (
|
||||
"log"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
)
|
||||
|
||||
// sceneKeysFor 推导本轮输入的**当前场景**。
|
||||
//
|
||||
// 场景是“这场面正在发生”的机器可读描述,用于把带条件的记忆(规则/约定)
|
||||
// 取回来。优先级:
|
||||
// 1. 注入点显式声明(payload.scene)——插件最清楚自己在什么场面里
|
||||
// 2. 通道(evt.Source → chan:qq)
|
||||
// 3. 工具(tool:qq_get_message)——工具输出触发的召回只知道这一步
|
||||
//
|
||||
// 多个场景是**并列命中**(取回任一场景的记忆),不是交集:
|
||||
// 「在 QQ 上」与「刚取回消息正文」是两个都能独立成立的触发条件。
|
||||
func sceneKeysFor(evt *agentIO.InputEvent, toolName string) []string {
|
||||
var keys []string
|
||||
seen := make(map[string]bool)
|
||||
add := func(k string) {
|
||||
// 显式声明的场景键来自插件,大小写/空白/标点都不可控;归一化后再去重,
|
||||
// 否则「chan:QQ」与「chan:qq」会变成两个场景,各自只召回一半记忆。
|
||||
k = memory.NormalizeSceneKey(k)
|
||||
if k == "" || seen[k] {
|
||||
return
|
||||
}
|
||||
seen[k] = true
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
if evt != nil && evt.Payload != nil {
|
||||
switch v := evt.Payload["scene"].(type) {
|
||||
case string:
|
||||
add(v)
|
||||
case []string:
|
||||
for _, s := range v {
|
||||
add(s)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
add(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if evt != nil {
|
||||
add(memory.ChannelScene(evt.Source))
|
||||
}
|
||||
if toolName != "" {
|
||||
add(memory.ToolScene(toolName))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// memoryPassOut 是一次记忆操作(取进来 / 踢出去)的结果。
|
||||
type memoryPassOut struct {
|
||||
@ -26,7 +81,7 @@ type memoryPassOut struct {
|
||||
// 稠密/词向量给已有事件打分,recall 用图 + TF-IDF 实体索引)。真正的
|
||||
// 「一次打分」要先统一打分空间(后续步骤);这里统一的是**入口、query、
|
||||
// 预算与审计**——这已是「一个过程」的可审计外壳,剩下的差在打分空间。
|
||||
func (a *Agent) memoryPass(query, trigger string, prune, recall bool) memoryPassOut {
|
||||
func (a *Agent) memoryPass(query, trigger string, prune, recall bool, scenes []string) memoryPassOut {
|
||||
var out memoryPassOut
|
||||
if a == nil || (!prune && !recall) {
|
||||
return out
|
||||
@ -35,7 +90,7 @@ func (a *Agent) memoryPass(query, trigger string, prune, recall bool) memoryPass
|
||||
out.Archived = a.pruneByQuery(query)
|
||||
}
|
||||
if recall && query != "" {
|
||||
out.RecallText = a.recallTextFor(query, trigger)
|
||||
out.RecallText = a.recallTextFor(query, trigger, scenes)
|
||||
}
|
||||
if out.Archived > 0 || out.RecallText != "" {
|
||||
log.Printf("[agent] memory pass (%s): archived=%d recalled=%d chars",
|
||||
|
||||
@ -22,7 +22,7 @@ func TestMemoryPass_NoPolicyIsNoOp(t *testing.T) {
|
||||
maxContextSize: 4,
|
||||
indexer: newTestIndexer(t, "咖啡", "张三"),
|
||||
}
|
||||
out := a.memoryPass("咖啡", "test", false, false)
|
||||
out := a.memoryPass("咖啡", "test", false, false, nil)
|
||||
if out.Archived != 0 || out.RecallText != "" {
|
||||
t.Fatalf("未声明任何策略时不应有任何输出,实际 %+v", out)
|
||||
}
|
||||
@ -31,7 +31,7 @@ func TestMemoryPass_NoPolicyIsNoOp(t *testing.T) {
|
||||
func TestMemoryPass_PruneAndRecallTogether(t *testing.T) {
|
||||
a := newMemoryPassAgent(t)
|
||||
before := a.context.Len()
|
||||
out := a.memoryPass("咖啡", "tool:test", true, true)
|
||||
out := a.memoryPass("咖啡", "tool:test", true, true, nil)
|
||||
if out.Archived == 0 {
|
||||
t.Fatal("声明 prune 应归档低相关事件")
|
||||
}
|
||||
@ -50,7 +50,7 @@ func TestMemoryPass_PoliciesAreOrthogonal(t *testing.T) {
|
||||
maxContextSize: 4,
|
||||
indexer: newTestIndexer(t, "咖啡", "张三"),
|
||||
}
|
||||
if out := onlyPrune.memoryPass("咖啡", "test", true, false); out.RecallText != "" {
|
||||
if out := onlyPrune.memoryPass("咖啡", "test", true, false, nil); out.RecallText != "" {
|
||||
t.Fatalf("只声明 prune 不应召回,实际 %q", out.RecallText)
|
||||
}
|
||||
// 只召回不裁剪:输出只有召回文本,上下文条数不变。
|
||||
@ -60,7 +60,7 @@ func TestMemoryPass_PoliciesAreOrthogonal(t *testing.T) {
|
||||
indexer: newTestIndexer(t, "咖啡", "张三"),
|
||||
}
|
||||
before := onlyRecall.context.Len()
|
||||
out := onlyRecall.memoryPass("咖啡", "test", false, true)
|
||||
out := onlyRecall.memoryPass("咖啡", "test", false, true, nil)
|
||||
if out.Archived != 0 {
|
||||
t.Fatalf("只声明 recall 不应裁剪,实际归档 %d", out.Archived)
|
||||
}
|
||||
|
||||
@ -90,15 +90,15 @@ func TestBuildTaskMemoryContext_RespectsPolicy(t *testing.T) {
|
||||
// 工具触发的召回:以(清洗后的)工具输出为 query,产出可注入的记忆文本。
|
||||
func TestRecallTextFor_UsesQuery(t *testing.T) {
|
||||
a := &Agent{indexer: newTestIndexer(t, "咖啡", "张三")}
|
||||
got := a.recallTextFor("咖啡", "tool:test")
|
||||
got := a.recallTextFor("咖啡", "tool:test", nil)
|
||||
if !strings.Contains(got, "【记忆索引】") {
|
||||
t.Fatalf("应产出记忆索引文本,实际 %q", got)
|
||||
}
|
||||
// 空 query 或无 indexer 时不产出、不 panic。
|
||||
if got := a.recallTextFor("", "tool:test"); got != "" {
|
||||
if got := a.recallTextFor("", "tool:test", nil); got != "" {
|
||||
t.Fatalf("空 query 应返回空串,实际 %q", got)
|
||||
}
|
||||
if got := (&Agent{}).recallTextFor("咖啡", "tool:test"); got != "" {
|
||||
if got := (&Agent{}).recallTextFor("咖啡", "tool:test", nil); got != "" {
|
||||
t.Fatalf("无 indexer 应返回空串,实际 %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
53
internal/agent/core/scene_test.go
Normal file
53
internal/agent/core/scene_test.go
Normal file
@ -0,0 +1,53 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
)
|
||||
|
||||
// TestSceneKeysFor 钉住当前场景的推导优先级:
|
||||
// 注入点显式声明 > 通道 > 工具;并列命中且去重。
|
||||
func TestSceneKeysFor(t *testing.T) {
|
||||
// 通道 + 工具:两个都能独立成立的触发条件,都要带上
|
||||
got := sceneKeysFor(&agentIO.InputEvent{Source: "qq"}, "qq_get_message")
|
||||
want := []string{"chan:qq", "tool:qq_get_message"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("sceneKeysFor = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("sceneKeysFor[%d] = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
|
||||
// 注入点显式声明排最前;归一化生效;重复声明去重
|
||||
evt := &agentIO.InputEvent{
|
||||
Source: "QQ",
|
||||
Payload: map[string]interface{}{"scene": " chan:qq/peer:group_1 "},
|
||||
}
|
||||
got = sceneKeysFor(evt, "")
|
||||
if len(got) != 2 || got[0] != "chan:qq/peer:group_1" || got[1] != "chan:qq" {
|
||||
t.Errorf("显式声明应排最前且通道场景归一: %v", got)
|
||||
}
|
||||
|
||||
// 数组形式声明
|
||||
evt = &agentIO.InputEvent{
|
||||
Source: "webui",
|
||||
Payload: map[string]interface{}{"scene": []interface{}{"chan:qq", "task:reminder"}},
|
||||
}
|
||||
got = sceneKeysFor(evt, "")
|
||||
if len(got) != 3 || got[0] != "chan:qq" || got[1] != "task:reminder" || got[2] != "chan:webui" {
|
||||
t.Errorf("数组声明未生效: %v", got)
|
||||
}
|
||||
|
||||
// nil 事件不 panic
|
||||
if got := sceneKeysFor(nil, ""); len(got) != 0 {
|
||||
t.Errorf("nil 事件应无场景: %v", got)
|
||||
}
|
||||
// 未声明的 payload 键不影响
|
||||
evt = &agentIO.InputEvent{Source: "cli", Payload: map[string]interface{}{"recall_policy": "none"}}
|
||||
if got := sceneKeysFor(evt, ""); len(got) != 1 || got[0] != "chan:cli" {
|
||||
t.Errorf("无 scene 声明时应只有通道场景: %v", got)
|
||||
}
|
||||
}
|
||||
@ -753,7 +753,11 @@ func (a *Agent) stepToolAfter(f *TaskFrame) stepOutcome {
|
||||
needRecall := def.RecallPolicy == sdk.RecallPolicyAuto
|
||||
if needPrune || needRecall {
|
||||
query := a.toolOutputForQuery(tc.Name, result)
|
||||
recallText = a.memoryPass(query, "tool:"+tc.Name, needPrune, needRecall).RecallText
|
||||
// 工具路召回的场景有两个来源:本轮输入的场面(如 chan:qq)
|
||||
// 与这一步工具本身(如 tool:qq_get_message)。带上工具场景,
|
||||
// 才能让「凡是要回 QQ 消息」这类规则在该步被取回。
|
||||
scenes := sceneKeysFor(f.Evt, tc.Name)
|
||||
recallText = a.memoryPass(query, "tool:"+tc.Name, needPrune, needRecall, scenes).RecallText
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -209,6 +209,11 @@ func (a *Agent) executeMemoryTool(tc agentAPI.ToolCall) string {
|
||||
if !ok {
|
||||
return "参数格式错误,需要 triples 数组"
|
||||
}
|
||||
// 场景键:模型可以在三元组里逐条给(scene 字段),也可以在工具参数
|
||||
// 顶层给一次(scene 参数),后者作为本批次的默认场景。
|
||||
// 两条路都为空则这条记忆不参与场景召回——不做猜测:猜错的场景会把
|
||||
// 无关记忆钉死,之后每次进入该场面都会被注入,比漏标更难发现。
|
||||
batchScene := getString(tc.Arguments, "scene")
|
||||
var triples []memory.Triple
|
||||
for _, td := range triplesData {
|
||||
if m, ok := td.(map[string]interface{}); ok {
|
||||
@ -217,6 +222,10 @@ func (a *Agent) executeMemoryTool(tc agentAPI.ToolCall) string {
|
||||
Relation: getString(m, "relation"),
|
||||
Object: getString(m, "object"),
|
||||
SentenceText: getString(m, "sentence_text"),
|
||||
Scene: getString(m, "scene"),
|
||||
}
|
||||
if t.Scene == "" {
|
||||
t.Scene = batchScene
|
||||
}
|
||||
// 模型显式关联的媒体:结构化字段随三元组一起提交,
|
||||
// 由 commitTriplesWithMedia 变成 L3 一等块并与句子建边——
|
||||
|
||||
@ -10,11 +10,11 @@ import (
|
||||
sdkmeta "gitcode.com/JianFeeeee/homeagent-sdk/meta"
|
||||
)
|
||||
|
||||
func (a *Agent) buildMemoryContext(input string, maxTokens int) string {
|
||||
func (a *Agent) buildMemoryContext(input string, maxTokens int, scenes []string) string {
|
||||
if a.indexer == nil {
|
||||
return ""
|
||||
}
|
||||
injected := a.indexer.BuildContext(input)
|
||||
injected := a.indexer.BuildContextInScene(input, scenes)
|
||||
s := a.indexer.FormatContext(injected)
|
||||
|
||||
// 图库召回命中的实体若关联着带媒体的句子,把媒体说明一并注入。
|
||||
@ -48,8 +48,9 @@ func (a *Agent) buildMemoryContext(input string, maxTokens int) string {
|
||||
// query 取**清洗后**的输入(通道 Cleaner 的输出),与裁剪侧同一套语义:
|
||||
// 原始输入里的 ANSI/base64/JSON 包装会把相关性打分带偏。清洗为空时回退原文。
|
||||
func (a *Agent) buildTaskMemoryContext(f *TaskFrame, input string, maxTokens int) string {
|
||||
scenes := sceneKeysFor(evtOf(f), "")
|
||||
if f == nil {
|
||||
return a.recallText(input, "input", maxTokens)
|
||||
return a.recallText(input, "input", maxTokens, scenes)
|
||||
}
|
||||
if !a.recallDeclared(f.Evt) {
|
||||
return ""
|
||||
@ -62,7 +63,15 @@ func (a *Agent) buildTaskMemoryContext(f *TaskFrame, input string, maxTokens int
|
||||
if f.Evt != nil && f.Evt.Source != "" {
|
||||
trigger = "input:" + f.Evt.Source
|
||||
}
|
||||
return a.recallText(query, trigger, maxTokens)
|
||||
return a.recallText(query, trigger, maxTokens, scenes)
|
||||
}
|
||||
|
||||
// evtOf 安全取出 TaskFrame 的事件(f 为 nil 时返回 nil)。
|
||||
func evtOf(f *TaskFrame) *agentIO.InputEvent {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
return f.Evt
|
||||
}
|
||||
|
||||
// recallTextFor 以 query 触发一次记忆召回,按当前预算截断,返回可注入的文本。
|
||||
@ -70,27 +79,27 @@ func (a *Agent) buildTaskMemoryContext(f *TaskFrame, input string, maxTokens int
|
||||
// 这是“召回”侧的单一入口:与 Prune 共用同一份**清洗后**的 query,
|
||||
// 使“取进来”(召回)与“踢出去”(裁剪)落在同一个相关性过程上。
|
||||
// trigger 仅用于日志溯源(如 "tool:qq_get_message")。
|
||||
func (a *Agent) recallTextFor(query, trigger string) string {
|
||||
func (a *Agent) recallTextFor(query, trigger string, scenes []string) string {
|
||||
memTokens := 0 // 0 = 不截断
|
||||
if a != nil && a.provider != nil {
|
||||
memTokens = ComputeTokenBudget(a.provider, a.systemPrompt).MemoryTokens
|
||||
}
|
||||
return a.recallText(query, trigger, memTokens)
|
||||
return a.recallText(query, trigger, memTokens, scenes)
|
||||
}
|
||||
|
||||
// recallText 是召回侧的共同实现:query → 记忆索引文本(空串表示无)。
|
||||
//
|
||||
// 输入侧的 buildTaskMemoryContext 与工具侧的 recallTextFor 都收敛到这里,
|
||||
// 使“同一份 query、同一次预算、同一条审计日志”只写一遍。
|
||||
func (a *Agent) recallText(query, trigger string, maxTokens int) string {
|
||||
func (a *Agent) recallText(query, trigger string, maxTokens int, scenes []string) string {
|
||||
if a == nil || query == "" || a.indexer == nil {
|
||||
return ""
|
||||
}
|
||||
text := a.buildMemoryContext(query, maxTokens)
|
||||
text := a.buildMemoryContext(query, maxTokens, scenes)
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
log.Printf("[agent] memory recall (%s): injected %d chars", trigger, len(text))
|
||||
log.Printf("[agent] memory recall (%s): injected %d chars (scenes=%v)", trigger, len(text), scenes)
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user