memory: 修注入开销、idf 排序与抽取诊断
注入侧 - context 在每次模型请求都触发,改为按 session + 最新用户消息去重: 一个 turn 里 n 次工具调用不再重复注入 n 个 memory 块 - 默认注入从 1200 字符 / 内联 4 条正文,降到 600 字符 / 内联 0 条: 大纲已说明存在什么,正文交给 memory_recall 按需取一次,避免每次 请求都为同一批正文付费 - halfLifeDays 此前被静默丢弃,注入始终按 14 天半衰期排序,现已接通 - 召回缓存 2s -> 500ms,同一工具链里 remember -> recall 能读到刚写的文件 排序 - idf 原先拿「单文件匹配数」当分子、「总文件数」当分母,每个文件得到 相同 idf,排序退化为纯词频;改为按文档频率计算的真实 idf 抽取 - 移除 400 段上限:它会静默截断长对话,300 轮只贡献前 ~50 轮 - role 在顶层或 message 下两种返回结构都读,之前只读 .role 得到空转录, 长度门槛不过,抽取看起来永远不触发 - 去重改按关键词重叠计分,不再用 bigram 重叠:模型改写与人工措辞几乎 不共享 bigram,真实改写只拿到 0.43;关键词是双方被迫共用的词汇 - 新增 memory_extract_dryrun:对真实模型跑完整抽取链路,不必改 minTurns 或等长会话 - 新增 memory_inspect:诊断记录按 scope 存入 storage key,并保留历史 与 fires/injected 计数,去重是否生效可被证伪 其他 - Context 改从 Plugin 命名空间导入,此前类型退化为 unknown - store 暴露 kind 字段;compileRootTables 内联进 compileRoot - frontmatter 列表元素统一 String(),index 不再出现数字 验证:由仓库所有者端到端验证(真实模型调用、真实会话);提交者未复跑, 以上为 diff 复核结论。
This commit is contained in:
+17
-24
@@ -45,8 +45,10 @@ export interface MemoryFile {
|
||||
readonly title: string
|
||||
/** One-line description shown in indexes and injected summaries. */
|
||||
readonly summary: string
|
||||
/** The "重点" column in index.md. */
|
||||
/** Highlight column in index.md. */
|
||||
readonly highlight: string
|
||||
/** `kind` from frontmatter, e.g. "fact", "rule", "rule-proposal". */
|
||||
readonly kind?: string
|
||||
/** Scenarios that should pull this file in. Drives ROOT's routing table. */
|
||||
readonly loadWhen: string[]
|
||||
readonly pinned: boolean
|
||||
@@ -170,6 +172,7 @@ function readMemoryFile(root: MemoryRoot, abs: string, category: string, name: s
|
||||
title: String(data.title ?? firstHeading(body) ?? slug),
|
||||
summary: String(data.summary ?? deriveSummary(body, slug)),
|
||||
highlight: String(data.highlight ?? ""),
|
||||
kind: data.kind === undefined ? undefined : String(data.kind),
|
||||
loadWhen: toStringArray(data.load_when ?? data.loadWhen),
|
||||
pinned: data.pinned === true || data.pinned === "true",
|
||||
created: toNumber(data.created) ?? stat.mtimeMs,
|
||||
@@ -431,12 +434,21 @@ function pruneEmptyIndexes(root: MemoryRoot, files: MemoryFile[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CompiledRoot {
|
||||
registry: string
|
||||
routing: string
|
||||
function describeCategory(category: string, files: MemoryFile[]): string {
|
||||
const pinned = files.find((f) => isHardRule(f))
|
||||
if (pinned && category === HARD_RULES_CATEGORY) return "不可违反的硬规则,常驻"
|
||||
const first = files[0]
|
||||
if (first?.fm.category_desc) return String(first.fm.category_desc)
|
||||
return first ? first.summary.slice(0, 40) : "(空)"
|
||||
}
|
||||
|
||||
export function compileRootTables(root: MemoryRoot, files: MemoryFile[]): CompiledRoot {
|
||||
/** Recompile ROOT.md's generated regions and write it if it does not exist. */
|
||||
export function compileRoot(root: MemoryRoot, files: MemoryFile[]): string {
|
||||
// Same reason as the category index: ROOT.md may be the first thing written
|
||||
// into a root that does not exist yet.
|
||||
fs.mkdirSync(root.dir, { recursive: true })
|
||||
const target = path.join(root.dir, "ROOT.md")
|
||||
|
||||
const categories = groupByCategory(files)
|
||||
|
||||
const registryRows = categories.map(({ category, files: list }) => {
|
||||
@@ -461,25 +473,6 @@ export function compileRootTables(root: MemoryRoot, files: MemoryFile[]): Compil
|
||||
? `\n| 场景 / 触发 | 读哪个文件 | 类型 |\n|---|---|---|\n${routingRows.join("\n")}\n`
|
||||
: `\n_暂无 load_when 标记的记忆。给文件加 frontmatter 的 \`load_when:\` 即可自动进入路由表。_\n`
|
||||
|
||||
return { registry, routing }
|
||||
}
|
||||
|
||||
function describeCategory(category: string, files: MemoryFile[]): string {
|
||||
const pinned = files.find((f) => isHardRule(f))
|
||||
if (pinned && category === HARD_RULES_CATEGORY) return "不可违反的硬规则,常驻"
|
||||
const first = files[0]
|
||||
if (first?.fm.category_desc) return String(first.fm.category_desc)
|
||||
return first ? first.summary.slice(0, 40) : "(空)"
|
||||
}
|
||||
|
||||
/** Recompile ROOT.md's generated regions and write it if it does not exist. */
|
||||
export function compileRoot(root: MemoryRoot, files: MemoryFile[]): string {
|
||||
// Same reason as the category index: ROOT.md may be the first thing written
|
||||
// into a root that does not exist yet.
|
||||
fs.mkdirSync(root.dir, { recursive: true })
|
||||
const target = path.join(root.dir, "ROOT.md")
|
||||
const { registry, routing } = compileRootTables(root, files)
|
||||
|
||||
let existing: string | undefined
|
||||
try {
|
||||
existing = fs.readFileSync(target, "utf8")
|
||||
|
||||
Reference in New Issue
Block a user