Skip to content

feat(memo): 新增跨会话持久记忆系统——模型工具与自动提取#245

Merged
phantom5099 merged 4 commits into
1024XEngineer:mainfrom
wynxing:main
Apr 13, 2026
Merged

feat(memo): 新增跨会话持久记忆系统——模型工具与自动提取#245
phantom5099 merged 4 commits into
1024XEngineer:mainfrom
wynxing:main

Conversation

@wynxing

@wynxing wynxing commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

概述

为 NeoCode 新增完整的跨会话持久记忆系统(internal/memo),对标 Claude Code 的记忆架构,使 Agent 能在会话间积累和利用知识。

主要变更

核心模块 (internal/memo/)

  • 存储层 (store.go): 基于文件系统的 FileStore,复用 session 的 SHA1 工作区隔离,原子写入
  • 索引读写 (index.go): MEMO.md 索引渲染与解析,Topic 文件 frontmatter 格式,NormalizeTitle 清洗
  • 服务层 (service.go): Service 编排 Add/Remove/List/Search/Recall,互斥锁保护 + 缓存失效回调
  • 上下文注入 (context_source.go): 实现 SectionSource 接口,将 MEMO.md 索引注入 system prompt,TTL 缓存
  • 领域类型 (types.go): 4 类记忆 (user/feedback/project/reference),Store/Extractor 接口
  • 规则提取器 (extractor.go): 检测用户消息中的显式信号词(记住/偏好/avoid 等),自动提取记忆

模型工具 (internal/tools/memo/)

  • memo_remember: Agent 主动保存记忆条目(type/title/content/keywords),Source=tool_initiated
  • memo_recall: Agent 按关键词搜索并加载记忆详情,返回 topic 文件完整内容

TUI 命令

  • /memo — 查看记忆索引
  • /remember <text> — 手动保存记忆
  • /forget <keyword> — 删除匹配记忆

Runtime 集成

  • Service 新增 MemoExtractor 接口与 SetMemoExtractor 方法
  • ReAct 循环 EventAgentDone 后异步调用 ExtractAndStore,失败静默处理
  • 仅当 AutoExtract 启用时注入

配置

  • MemoConfigenabled/auto_extract/max_index_lines,支持 YAML 持久化
  • 默认 enabled=true, auto_extract=true, max_index_lines=200

其他改进(前期合并)

  • Provider OpenAI 兼容层重构
  • TUI workspace 切换命令与解析器
  • Context builder 增强、Session workspace 导出

存储布局

~/.neocode/projects/<workspace_hash>/memo/
  MEMO.md          # 索引(~200 行上限,始终注入上下文)
  topics/          # 详细内容文件,按需加载

架构

用户对话 → Runtime ReAct 循环
              ├─ EventAgentDone → RuleExtractor → Service.Add(异步提取)
              ├─ 每轮开始 → ContextBuilder.Build → MemoSource.Sections(注入索引)
              └─ 模型调用 → memo_remember / memo_recall 工具

用户手动 → /memo /remember /forget → MemoService

测试覆盖

  • internal/memo/ 全部单元测试(types/store/index/service/context_source/extractor)
  • internal/tools/memo/ 全部单元测试(remember/recall 正常+边界+异常)
  • internal/config/ MemoConfig Clone/Validate/ApplyDefaults 测试
  • internal/context/ NewBuilderWithMemo 测试
  • internal/tui/core/app/ memo 命令处理函数测试
  • go build ./... && go test ./... 全量通过

测试计划

  • 单元测试全部通过
  • 启动应用,/remember 我喜欢中文注释,重启后 /memo 仍可见
  • Agent 回复中引用记忆内容
  • 用户说"记住以后都用 tab"后,下次会话自动提取生效
  • /forget 删除后确认不再出现在上下文

wynxing and others added 3 commits April 12, 2026 21:48
Agent 可通过工具调用主动读写跨会话记忆:
- memo_remember: 保存记忆条目(type/title/content/keywords)
- memo_recall: 按关键词搜索并加载记忆详情
- 工具通过构造函数注入 memo.Service,条件注册(仅 Memo.Enabled)
- MicroCompactPolicy=PreserveHistory,保留记忆读写结果

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 新增 RuleExtractor:检测用户消息中的显式信号词(记住/偏好/avoid 等)
- runtime.Service 新增 MemoExtractor 接口与 SetMemoExtractor 方法
- EventAgentDone 后异步调用 ExtractAndStore,失败静默处理
- bootstrap 条件注入(仅 AutoExtract 启用时)
- 完整单元测试覆盖:信号检测、英文信号、长内容截断、边界条件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fennoai[bot]

This comment was marked as outdated.

合并 1024XEngineer/neo-code main (2079c91),解决 1 处冲突:
- bootstrap.go: 合并 memotool 和 mcp 两个新增 import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@wynxing

wynxing commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator Author

/simplify

@fennoai

fennoai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Completed

已按请求完成:检查历史评论中的未解决 review 项、修复值得修复的问题,并执行 code-simplifier 复审后继续简化实现。

What I changed

  • 修复 internal/memo/extractor.go:标题截断改为 rune-safe,避免中文等多字节 UTF-8 截断损坏;并预计算小写信号词,简化并减少重复 ToLower
  • 修复 internal/runtime/runtime.go
    • 记忆提取仅传入本轮 userMessage(不再复制/扫描整个 session.Messages)。
    • 增加异步提取并发限流(槽位)和超时上下文,避免后台无上限膨胀。
  • 修复 internal/tools/memo/remember.go / internal/tools/memo/recall.go:增加 svc == nil 防护,避免配置异常时 panic。
  • 修复 internal/tools/memo/recall.go:返回结果增加 tools.ApplyOutputLimit(..., tools.DefaultOutputLimitBytes) 限制。
  • 简化:新增 common.go 收敛 memo 工具的统一 nil-service 错误构造,去除重复代码。

Tests

  • 新增/更新回归测试,覆盖:
    • UTF-8 安全截断
    • remember/recall 的 nil service 防护
    • recall 输出截断
    • runtime 记忆提取仅接收本轮用户消息
  • 已通过:go test ./...

Git

Not done

  • 未额外处理 trigger comment 之外的新需求(仅处理了当前指令要求范围)。

View job run

@codecov

codecov Bot commented Apr 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.92547% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/runtime/runtime.go 0.00% 5 Missing and 1 partial ⚠️
internal/tools/memo/remember.go 95.00% 3 Missing ⚠️
internal/app/bootstrap.go 77.77% 2 Missing ⚠️
internal/tools/memo/recall.go 95.83% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@phantom5099

Copy link
Copy Markdown
Collaborator

/review -codex

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

发现几处需要修正的风险点,主要集中在记忆提取的生命周期/资源控制、UTF-8 截断正确性、以及 memo_recall 输出上限控制。另有文档同步缺口:本 PR 引入了 memo 工具与运行时后置持久化行为,但未同步更新相应 docs。

s.emit(ctx, EventAgentDone, input.RunID, session.ID, assistant)
// 异步提取记忆:不影响主循环,失败静默处理。
if s.memoExtractor != nil {
msgs := make([]providertypes.Message, len(session.Messages))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里为提取记忆每轮都复制整个 session.Messages。当前提取器只用到“最后一条 user 消息”,全量 make+copy 在长会话会造成不必要的 O(n) 分配与拷贝。建议改为仅传递提取所需最小数据(例如最后一条用户消息)。

if s.memoExtractor != nil {
msgs := make([]providertypes.Message, len(session.Messages))
copy(msgs, session.Messages)
go s.memoExtractor.ExtractAndStore(context.Background(), msgs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go ...ExtractAndStore(context.Background(), msgs) 是无背压的 detached goroutine:高频回合会持续创建 goroutine 并触发磁盘写入,且与调用方生命周期脱钩。建议使用受控 worker/队列或显式并发上限,并绑定可管理的上下文。

// 截断过长内容作为标题
title := NormalizeTitle(lastUserMsg)
if len(title) > 150 {
title = title[:147] + "..."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里按字节截断字符串(title[:147]),对 UTF-8 多字节字符可能切断 rune,导致标题出现非法编码/替换字符。建议按 rune 数截断。

{
Type: TypeUser,
Title: title,
Content: lastUserMsg,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自动提取会将整条用户消息原文持久化到记忆。结合较宽泛的信号词(如 always/prefer),容易把敏感信息误存为长期数据。建议在入库前增加敏感信息过滤/脱敏策略,或缩窄自动提取触发条件。

}
sort.Strings(keys)

var builder strings.Builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

memo_recall 会拼接所有匹配 topic 的完整内容且结果被 PreserveHistory 保留,缺少条目数/字节数上限时会放大上下文与 token 成本。建议增加返回上限与截断策略(并提示“部分结果已省略”)。

@phantom5099 phantom5099 merged commit 5581488 into 1024XEngineer:main Apr 13, 2026
2 checks passed
wynxing added a commit to wynxing/neo-code that referenced this pull request Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants