Commit c45e4968ce99ae34497882b18c805f3420814d6e
1 parent
c0955699
fix(memory): prioritize current task relative to history for hippocampus; optimi…
…ze Chinese keyword extraction - Fix: The long trailing history in todo.md drowned out the actual current task keywords because English tokens (e.g. TASK, P2, from) padded the 30-keyword limit beforehand. - Updated extractKeywords: Process Chinese characters first as they carry more weight for NLP context intent. - Added userMessage prefix to searchText in buildSystemPrompt so newly dispatched tasks dominate keyword extraction. - End-to-end e2e test created and passed (27/27 asserts passed)
Showing
15 changed files
with
670 additions
and
28 deletions
data/agents/poet/.claude/scheduled_tasks.lock
0 → 100644
data/agents/poet/memory/cerebellum/为国庆节创作一组主题诗集-包含5首诗.md
0 → 100644
| 1 | +# 方案: 为国庆节创作一组主题诗集,包含5首诗 | |
| 2 | + | |
| 3 | +> 创建时间: 2026-03-25 | |
| 4 | + | |
| 5 | +## 适用条件 | |
| 6 | +- 类似任务: 为国庆节创作一组主题诗集,包含5首诗 | |
| 7 | + | |
| 8 | +## 执行步骤 | |
| 9 | +1. 确定国庆主题和5个子题材(国旗、山河、英雄、和平、未来) | |
| 10 | +2. 分别确定每首诗的体裁(五绝、七绝、五律、七律、词) | |
| 11 | +3. 创作第一首《旗颂》五言绝句 | |
| 12 | +4. 创作第二首《山河赋》七言律诗 | |
| 13 | +5. 创作第三首《英雄辞》五言律诗 | |
| 14 | +6. 创作第四首《和平吟》七言绝句 | |
| 15 | +7. 创作第五首《展望》词·水调歌头 | |
| 16 | +8. 整体校对格律和意境 | |
| 17 | + | |
| 18 | +## 注意事项 | |
| 19 | +- 自动从对话 test-complex 提取 | |
| 20 | +- 耗时 180秒, 8 次工具调用 | ... | ... |
data/agents/poet/memory/cerebellum/写古典诗词.md
0 → 100644
data/agents/poet/memory/daily/2026-03-25.md
0 → 100644
| 1 | +# 日报 — 2026-03-25 | |
| 2 | + | |
| 3 | +## 概览 | |
| 4 | +- 对话数: 3 | |
| 5 | +- 反思数: 2 | |
| 6 | +- 新模式: 2 | |
| 7 | +- 新教训: 2 | |
| 8 | +- 固化方案: 1 | |
| 9 | + | |
| 10 | +## 对话记录 | |
| 11 | + | |
| 12 | +### 2026-03-25 03:48:29 [completed] (2 次, 47秒) | |
| 13 | +- 任务: [TASK from master] | |
| 14 | +任务编号: T-mn5i6b4q | |
| 15 | +优先级: P2 | |
| 16 | + | |
| 17 | +请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | |
| 18 | + | |
| 19 | +请立即执行此任务。完成后调用 c | |
| 20 | +- 结果: ...墨为君绘就一方诗意天地。 | |
| 21 | + | |
| 22 | +先恢复我的定时任务,然后为您创作这首踏青绝句。 定时任务已恢复。现在为您创作这首踏青绝句: | |
| 23 | + | |
| 24 | +--- | |
| 25 | + | |
| 26 | +【作品】 | |
| 27 | +《踏青》 | |
| 28 | +(五言绝句,平水韵 十三元) | |
| 29 | + | |
| 30 | +草色侵衣绿,花风拂面温。 | |
| 31 | +踏青人醉处,春在小溪痕。 | |
| 32 | + | |
| 33 | +【格律】 | |
| 34 | +``` | |
| 35 | +平仄:仄仄平平仄,平平仄仄平。 | |
| 36 | + 仄平平仄仄,平仄仄平平。 | |
| 37 | +押韵:温、痕(上平声 十三元韵部) | |
| 38 | +``` | |
| 39 | + | |
| 40 | +【简评】 | |
| 41 | +此诗以" | |
| 42 | + | |
| 43 | +### 2026-03-25 03:49:14 [completed] (1 次, 42秒) | |
| 44 | +- 任务: [TASK from master] | |
| 45 | +任务编号: T-mn5i7dqk | |
| 46 | +优先级: P2 | |
| 47 | + | |
| 48 | +你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | |
| 49 | + | |
| 50 | +请立即执行此任务。 | |
| 51 | +- 结果: 我记得刚才创作了一首关于春天踏青的五言绝句。根据我的近期记忆,任务编号 **T-mn5i6b4q** 中我完成了以下作品: | |
| 52 | + | |
| 53 | +--- | |
| 54 | + | |
| 55 | +**【作品】《踏青》** | |
| 56 | +(五言绝句,平水韵 十三元) | |
| 57 | + | |
| 58 | +草色侵衣绿,花风拂面温。 | |
| 59 | +踏青人醉处,春在小溪痕。 | |
| 60 | + | |
| 61 | +--- | |
| 62 | + | |
| 63 | +此诗通过「草色」「花风」「溪痕」等意象,描绘春日踏青时衣染草绿、面拂花香的惬意场景,末句以"春在小溪痕"收束,将抽象的春意化为流水留下的痕迹, | |
| 64 | + | |
| 65 | +### 2026-03-25 03:49:50 [completed] (1 次, 33秒) | |
| 66 | +- 任务: [TASK from master] | |
| 67 | +任务编号: T-mn5i8d96 | |
| 68 | +优先级: P2 | |
| 69 | + | |
| 70 | +请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | |
| 71 | + | |
| 72 | +请立即执行此任务。完成后调用 curl 汇报: | |
| 73 | +- 结果: 在下诗人,愿以笔墨为君绘就一方诗意天地。 | |
| 74 | + | |
| 75 | +--- | |
| 76 | + | |
| 77 | +【作品】 | |
| 78 | +《中秋望月》 | |
| 79 | +(七言绝句,平水韵 十四寒) | |
| 80 | + | |
| 81 | +玉轮初上碧云端,万里清辉照画栏。 | |
| 82 | +此夜人间同仰望,不知秋思落谁边。 | |
| 83 | + | |
| 84 | +【格律】 | |
| 85 | +``` | |
| 86 | +平仄:仄平平仄仄平平,仄仄平平仄仄平。 | |
| 87 | + 仄仄平平平仄仄,仄平平仄仄平平。 | |
| 88 | +押韵:端、栏、边(上平声 十四寒韵部) | |
| 89 | +``` | |
| 90 | + | |
| 91 | +【简评】 | |
| 92 | +首句"玉轮初上"点出中秋月升之景,次句"万里清辉"写月光 | |
| 93 | + | |
| 94 | +## 反思摘要 | |
| 95 | + | |
| 96 | +- ✅ 成功创建/修改了 2 个文件: workspace/poet/guoqing-collection.md, workspace/poet/guoqing-notes.md | |
| 97 | +- ✅ 完成了 8 步工具调用的复杂任务 (180秒) | |
| 98 | +- ❌ 错误: 输入格式无法解析 Error: Invalid poem format at line 3 | |
| 99 | +- ❌ 任务失败 (4 tool calls, 60秒): 翻译古诗为英文但格式错误 | ... | ... |
data/agents/poet/memory/hippocampus-index.json
0 → 100644
| 1 | +[ | |
| 2 | + { | |
| 3 | + "scenario": "写古典诗词", | |
| 4 | + "keywords": [ | |
| 5 | + "写诗", | |
| 6 | + "诗词", | |
| 7 | + "古诗", | |
| 8 | + "五言", | |
| 9 | + "七言", | |
| 10 | + "绝句", | |
| 11 | + "律诗", | |
| 12 | + "创作" | |
| 13 | + ], | |
| 14 | + "planFile": "写古典诗词.md", | |
| 15 | + "confidence": 0.9, | |
| 16 | + "useCount": 1, | |
| 17 | + "lastUsed": "2026-03-25", | |
| 18 | + "createdAt": "2026-03-25" | |
| 19 | + }, | |
| 20 | + { | |
| 21 | + "scenario": "为国庆节创作一组主题诗集,包含5首诗", | |
| 22 | + "keywords": [ | |
| 23 | + "为国", | |
| 24 | + "国庆", | |
| 25 | + "庆节", | |
| 26 | + "节创", | |
| 27 | + "创作", | |
| 28 | + "作一", | |
| 29 | + "一组", | |
| 30 | + "组主" | |
| 31 | + ], | |
| 32 | + "planFile": "为国庆节创作一组主题诗集-包含5首诗.md", | |
| 33 | + "confidence": 0.76, | |
| 34 | + "useCount": 0, | |
| 35 | + "lastUsed": null, | |
| 36 | + "createdAt": "2026-03-25" | |
| 37 | + } | |
| 38 | +] | |
| 0 | 39 | \ No newline at end of file | ... | ... |
data/agents/poet/memory/hippocampus.md
| 1 | 1 | # 海马体 — 经验记忆索引 |
| 2 | 2 | |
| 3 | 3 | > 记录过往处理过的情景模式。遇到相似任务时,优先调用小脑中的固化方案。 |
| 4 | +> ⚠️ 此文件由系统自动生成,勿手动编辑。真实数据在 hippocampus-index.json。 | |
| 4 | 5 | |
| 5 | -| # | 情景模式 | 匹配条件 | 方案文件 | 置信度 | 使用次数 | 上次使用 | | |
| 6 | -|---|---------|---------|---------|--------|---------|----------| | |
| 7 | -| | (暂无经验记录) | | | | | | | |
| 6 | +| # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 | | |
| 7 | +|---|---------|-----------|---------|--------|---------|----------| | |
| 8 | +| 1 | 写古典诗词 | 写诗, 诗词, 古诗, 五言, 七言, 绝句, 律诗, 创作 | 写古典诗词.md | 0.9 | 1 | 2026-03-25 | | |
| 9 | +| 2 | 为国庆节创作一组主题诗集,包含5首诗 | 为国, 国庆, 庆节, 节创, 创作, 作一, 一组, 组主 | 为国庆节创作一组主题诗集-包含5首诗.md | 0.76 | 0 | - | | ... | ... |
data/agents/poet/memory/long-term/consolidate-2026-03-25.md
0 → 100644
| 1 | +# 长期记忆合并 — 2026-03-25 | |
| 2 | +> 合并了 3 个过期 session (>7 天) | |
| 3 | +## 高频关键词 | |
| 4 | +- 任务 (x3) | |
| 5 | +- 旧任务 (x3) | |
| 6 | +- 旧任 (x3) | |
| 7 | +- 写一 (x3) | |
| 8 | +- 一首 (x3) | |
| 9 | +- 首关 (x3) | |
| 10 | +- 关于 (x3) | |
| 11 | +- 于秋 (x3) | |
| 12 | +- 秋天 (x3) | |
| 13 | +- 天的 (x3) | |
| 14 | +## 合并的对话摘要 | |
| 15 | +### 2026-03-17 00:00:00 [] (, ) | |
| 16 | +- 任务: 旧任务-0: 写一首关于秋天的诗 | |
| 17 | +- 结果: 秋诗完成-0 | |
| 18 | + | |
| 19 | +### 2026-03-17 01:00:00 [] (, ) | |
| 20 | +- 任务: 旧任务-1: 写一首关于秋天的诗 | |
| 21 | +- 结果: 秋诗完成-1 | |
| 22 | + | |
| 23 | +### 2026-03-17 02:00:00 [] (, ) | |
| 24 | +- 任务: 旧任务-2: 写一首关于秋天的诗 | |
| 25 | +- 结果: 秋诗完成-2 | ... | ... |
data/agents/poet/memory/long-term/patterns.md
0 → 100644
data/agents/poet/memory/memory.md
| 1 | 1 | # 我的记忆 |
| 2 | 2 | |
| 3 | 3 | ## 关键事实 |
| 4 | -(尚无记录) | |
| 4 | +- [2026-03-25] [TASK from master] → ...墨为君绘就一方诗意天地。 | |
| 5 | +- [2026-03-25] [TASK from master] → 我记得刚才创作了一首关于春天踏青的五言绝句。根据我的近期记忆,任务编号 **T-mn5i6b4q** 中我完成了以下作品: | |
| 6 | +- [2026-03-25] [TASK from master] → 在下诗人,愿以笔墨为君绘就一方诗意天地。 | |
| 5 | 7 | |
| 6 | 8 | ## 已验证的模式 |
| 7 | -(尚无记录) | |
| 9 | +- [2026-03-25] 成功创建/修改了 2 个文件: workspace/poet/guoqing-collection.md, workspace/poet/guoqing-notes.md | |
| 10 | +- [2026-03-25] 完成了 8 步工具调用的复杂任务 (180秒) | |
| 8 | 11 | |
| 9 | 12 | ## 失败教训 |
| 10 | -(尚无记录) | |
| 13 | +- [2026-03-25] 错误: 输入格式无法解析 Error: Invalid poem format at line 3 | |
| 14 | +- [2026-03-25] 任务失败 (4 tool calls, 60秒): 翻译古诗为英文但格式错误 | ... | ... |
data/agents/poet/memory/reflections/reflect-1774410599260.json
0 → 100644
| 1 | +{ | |
| 2 | + "timestamp": "2026-03-25T03:49:59.259Z", | |
| 3 | + "convId": "test-complex", | |
| 4 | + "task": "为国庆节创作一组主题诗集,包含5首诗", | |
| 5 | + "status": "completed", | |
| 6 | + "toolCount": 8, | |
| 7 | + "durationSec": 180, | |
| 8 | + "patterns": [ | |
| 9 | + "成功创建/修改了 2 个文件: workspace/poet/guoqing-collection.md, workspace/poet/guoqing-notes.md", | |
| 10 | + "完成了 8 步工具调用的复杂任务 (180秒)" | |
| 11 | + ], | |
| 12 | + "lessons": [], | |
| 13 | + "plan": { | |
| 14 | + "scenario": "为国庆节创作一组主题诗集,包含5首诗", | |
| 15 | + "keywords": [ | |
| 16 | + "为国", | |
| 17 | + "国庆", | |
| 18 | + "庆节", | |
| 19 | + "节创", | |
| 20 | + "创作", | |
| 21 | + "作一", | |
| 22 | + "一组", | |
| 23 | + "组主" | |
| 24 | + ], | |
| 25 | + "steps": [ | |
| 26 | + "确定国庆主题和5个子题材(国旗、山河、英雄、和平、未来)", | |
| 27 | + "分别确定每首诗的体裁(五绝、七绝、五律、七律、词)", | |
| 28 | + "创作第一首《旗颂》五言绝句", | |
| 29 | + "创作第二首《山河赋》七言律诗", | |
| 30 | + "创作第三首《英雄辞》五言律诗", | |
| 31 | + "创作第四首《和平吟》七言绝句", | |
| 32 | + "创作第五首《展望》词·水调歌头", | |
| 33 | + "整体校对格律和意境" | |
| 34 | + ], | |
| 35 | + "confidence": 0.76 | |
| 36 | + } | |
| 37 | +} | |
| 0 | 38 | \ No newline at end of file | ... | ... |
data/agents/poet/memory/reflections/reflect-1774410599261.json
0 → 100644
| 1 | +{ | |
| 2 | + "timestamp": "2026-03-25T03:49:59.260Z", | |
| 3 | + "convId": "test-fail", | |
| 4 | + "task": "翻译古诗为英文但格式错误", | |
| 5 | + "status": "failed", | |
| 6 | + "toolCount": 4, | |
| 7 | + "durationSec": 60, | |
| 8 | + "patterns": [], | |
| 9 | + "lessons": [ | |
| 10 | + "错误: 输入格式无法解析 Error: Invalid poem format at line 3", | |
| 11 | + "任务失败 (4 tool calls, 60秒): 翻译古诗为英文但格式错误" | |
| 12 | + ], | |
| 13 | + "plan": null | |
| 14 | +} | |
| 0 | 15 | \ No newline at end of file | ... | ... |
data/agents/poet/todo.md
| ... | ... | @@ -5,3 +5,25 @@ |
| 5 | 5 | | # | 任务 | 状态 | 分配人 | 优先级 | 接收时间 | 上次检查 | |
| 6 | 6 | |---|------|------|--------|--------|----------|----------| |
| 7 | 7 | | T-mn405cps | 测试你的诗词创作功能。请创作一首七言绝句,主题:春天,情感基调:明快。要求:1) 严格按照七律格律;2) 标注平仄和韵部;3) 提供简评赏析 | ✅ 完成 | master-001 | P1 | 2026-03-24 02:35 | - | |
| 8 | +| T-mn5h7iux | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:20 | - | | |
| 9 | +| T-mn5h8dr7 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:21 | - | | |
| 10 | +| T-mn5h95kh | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:21 | - | | |
| 11 | +| T-mn5hcpbb | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:24 | - | | |
| 12 | +| T-mn5hdrxo | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:25 | - | | |
| 13 | +| T-mn5hei6x | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:26 | - | | |
| 14 | +| T-mn5hgw6y | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:27 | - | | |
| 15 | +| T-mn5hkltv | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:30 | - | | |
| 16 | +| T-mn5hlt3f | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:31 | - | | |
| 17 | +| T-mn5hmx9j | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:32 | - | | |
| 18 | +| T-mn5hpbq3 | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:34 | - | | |
| 19 | +| T-mn5hqqp4 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:35 | - | | |
| 20 | +| T-mn5hr98v | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:35 | - | | |
| 21 | +| T-mn5hxnyf | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:40 | - | | |
| 22 | +| T-mn5hyke4 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:41 | - | | |
| 23 | +| T-mn5hzjx9 | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:42 | - | | |
| 24 | +| T-mn5i2hot | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:44 | - | | |
| 25 | +| T-mn5i3ne9 | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:45 | - | | |
| 26 | +| T-mn5i4rjr | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:46 | - | | |
| 27 | +| T-mn5i6b4q | 请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。 | ✅ 完成 | master | P2 | 2026-03-25 03:47 | - | | |
| 28 | +| T-mn5i7dqk | 你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:48 | - | | |
| 29 | +| T-mn5i8d96 | 请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。 | ✅ 完成 | master | P2 | 2026-03-25 03:49 | - | | ... | ... |
server/routes/agent-chat.js
| ... | ... | @@ -241,7 +241,7 @@ const BASE_DIR = path.join(__dirname, '../../packages/agent-core/base'); |
| 241 | 241 | * - tools.md: base/common/tools-base.md + agent/tools.md (追加) |
| 242 | 242 | * - 其他文件 (soul.md, identity.md 等): 只从 agent/ 读取(纯个性化) |
| 243 | 243 | */ |
| 244 | -function buildSystemPrompt(agentDir) { | |
| 244 | +function buildSystemPrompt(agentDir, userMessage) { | |
| 245 | 245 | const config = loadAgentConfig(agentDir); |
| 246 | 246 | const agentType = config?.type || 'skill'; |
| 247 | 247 | |
| ... | ... | @@ -298,9 +298,10 @@ function buildSystemPrompt(agentDir) { |
| 298 | 298 | if (recentMemory) parts.push('---\n\n' + recentMemory); |
| 299 | 299 | |
| 300 | 300 | // hippocampus — 海马体检索相关经验 (Phase 2) |
| 301 | - // 使用当前任务文本作为检索 key(从 todo.md 或最后一条消息提取) | |
| 301 | + // 将 userMessage 放在前,避免 todo.md 历史堆积导致当前任务的关键词被截断 | |
| 302 | 302 | const todoContent = loadFile(agentDir, 'todo.md'); |
| 303 | - const hippocampusPlans = retrieveRelevantPlans(agentIdForMemory, todoContent); | |
| 303 | + const searchText = [userMessage || '', todoContent].join('\n\n'); | |
| 304 | + const hippocampusPlans = retrieveRelevantPlans(agentIdForMemory, searchText); | |
| 304 | 305 | if (hippocampusPlans) parts.push('---\n\n' + hippocampusPlans); |
| 305 | 306 | |
| 306 | 307 | // skills |
| ... | ... | @@ -551,7 +552,7 @@ function buildPrompt(agentDir, agentId, convId, userMessage, isNewSession) { |
| 551 | 552 | return userMessage; |
| 552 | 553 | } |
| 553 | 554 | |
| 554 | - const systemPrompt = buildSystemPrompt(agentDir); | |
| 555 | + const systemPrompt = buildSystemPrompt(agentDir, userMessage); | |
| 555 | 556 | const cronInstructions = buildCronInstructions(agentDir); |
| 556 | 557 | const history = loadHistory(agentId, convId); |
| 557 | 558 | ... | ... |
server/scripts/test-memory-e2e.js
0 → 100644
| 1 | +#!/usr/bin/env node | |
| 2 | +/** | |
| 3 | + * Memory System — 完整端到端测试 | |
| 4 | + * | |
| 5 | + * 用真实 Agent 对话验证三个 Phase 的完整效果: | |
| 6 | + * | |
| 7 | + * Phase 1 验证: | |
| 8 | + * ① 对话完成 → short-term/ 自动出现 session 文件 | |
| 9 | + * ② memory.md 自动追加关键事实 | |
| 10 | + * ③ 第二次对话 → Agent 能引用第一次的内容(近期记忆注入) | |
| 11 | + * | |
| 12 | + * Phase 2 验证: | |
| 13 | + * ④ 保存小脑方案 → hippocampus-index.json 更新 | |
| 14 | + * ⑤ 发送匹配任务 → 海马体命中方案 → prompt 体积增大 | |
| 15 | + * ⑥ 使用次数自动递增 | |
| 16 | + * | |
| 17 | + * Phase 3 验证: | |
| 18 | + * ⑦ 复杂任务(多 tool call)→ 自动触发反思 → reflections/ 出现日志 | |
| 19 | + * ⑧ memory.md "已验证模式" / "失败教训" 段被更新 | |
| 20 | + * ⑨ 过期 short-term 合并 → long-term/ 出现合并记录 | |
| 21 | + * ⑩ 日报 API → daily/{today}.md 生成 | |
| 22 | + */ | |
| 23 | + | |
| 24 | +const http = require('http'); | |
| 25 | +const fs = require('fs'); | |
| 26 | +const path = require('path'); | |
| 27 | + | |
| 28 | +const BASE = 'http://localhost:3000'; | |
| 29 | +const AGENT = 'poet'; // 用 poet 做测试(轻量级、不影响 ceo 数据) | |
| 30 | +const DATA_DIR = path.resolve(__dirname, '../../data/agents'); | |
| 31 | +const AGENT_DIR = path.join(DATA_DIR, AGENT); | |
| 32 | +const MEM = path.join(AGENT_DIR, 'memory'); | |
| 33 | + | |
| 34 | +let passed = 0, failed = 0, total = 0; | |
| 35 | + | |
| 36 | +function test(condition, msg) { | |
| 37 | + total++; | |
| 38 | + if (condition) { passed++; console.log(` ✅ ${msg}`); } | |
| 39 | + else { failed++; console.log(` ❌ FAIL: ${msg}`); } | |
| 40 | +} | |
| 41 | + | |
| 42 | +// ── HTTP 工具 ── | |
| 43 | +function post(urlStr, body) { | |
| 44 | + return new Promise((resolve, reject) => { | |
| 45 | + const data = JSON.stringify(body); | |
| 46 | + const u = new URL(urlStr); | |
| 47 | + const req = http.request({ | |
| 48 | + hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST', | |
| 49 | + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } | |
| 50 | + }, res => { | |
| 51 | + let buf = ''; | |
| 52 | + res.on('data', c => buf += c); | |
| 53 | + res.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve(buf); } }); | |
| 54 | + }); | |
| 55 | + req.on('error', reject); | |
| 56 | + req.write(data); | |
| 57 | + req.end(); | |
| 58 | + }); | |
| 59 | +} | |
| 60 | + | |
| 61 | +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } | |
| 62 | + | |
| 63 | +function waitForStatus(agentId, convId, timeoutMs = 120000) { | |
| 64 | + const start = Date.now(); | |
| 65 | + return new Promise((resolve, reject) => { | |
| 66 | + const check = () => { | |
| 67 | + if (Date.now() - start > timeoutMs) return reject(new Error(`Timeout waiting for ${convId}`)); | |
| 68 | + try { | |
| 69 | + const metaPath = path.join(DATA_DIR, agentId, 'conversations', convId, 'meta.json'); | |
| 70 | + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); | |
| 71 | + if (meta.status === 'completed' || meta.status === 'failed') return resolve(meta); | |
| 72 | + } catch {} | |
| 73 | + setTimeout(check, 2000); | |
| 74 | + }; | |
| 75 | + check(); | |
| 76 | + }); | |
| 77 | +} | |
| 78 | + | |
| 79 | +function readFile(p) { try { return fs.readFileSync(p, 'utf-8'); } catch { return ''; } } | |
| 80 | +function listDir(p) { try { return fs.readdirSync(p); } catch { return []; } } | |
| 81 | + | |
| 82 | +// ════════════════════════════════════════ | |
| 83 | +// Clean Slate | |
| 84 | +// ════════════════════════════════════════ | |
| 85 | +function cleanMemory() { | |
| 86 | + const dirs = ['short-term', 'long-term', 'reflections', 'cerebellum', 'daily']; | |
| 87 | + for (const d of dirs) { | |
| 88 | + const dir = path.join(MEM, d); | |
| 89 | + if (fs.existsSync(dir)) { | |
| 90 | + for (const f of fs.readdirSync(dir)) { | |
| 91 | + if (f === '.' || f === '..') continue; | |
| 92 | + try { fs.unlinkSync(path.join(dir, f)); } catch {} | |
| 93 | + } | |
| 94 | + } | |
| 95 | + } | |
| 96 | + // 重置 memory.md | |
| 97 | + fs.writeFileSync(path.join(MEM, 'memory.md'), | |
| 98 | + '# 我的记忆\n\n## 关键事实\n(尚无记录)\n\n## 已验证的模式\n(尚无记录)\n\n## 失败教训\n(尚无记录)\n', 'utf-8'); | |
| 99 | + // 清 hippocampus | |
| 100 | + const hippIdx = path.join(MEM, 'hippocampus-index.json'); | |
| 101 | + if (fs.existsSync(hippIdx)) fs.unlinkSync(hippIdx); | |
| 102 | +} | |
| 103 | + | |
| 104 | +// ════════════════════════════════════════ | |
| 105 | +// Main Test | |
| 106 | +// ════════════════════════════════════════ | |
| 107 | +async function main() { | |
| 108 | + console.log('\n🧠 Memory System — 端到端集成测试'); | |
| 109 | + console.log('═'.repeat(56)); | |
| 110 | + console.log(` Agent: ${AGENT}`); | |
| 111 | + console.log(` Server: ${BASE}`); | |
| 112 | + console.log('═'.repeat(56)); | |
| 113 | + | |
| 114 | + // ── 0. Clean ── | |
| 115 | + console.log('\n🗑️ Step 0: 清理历史数据'); | |
| 116 | + cleanMemory(); | |
| 117 | + console.log(' Done.\n'); | |
| 118 | + | |
| 119 | + // ═══════════════════════════════════ | |
| 120 | + // PHASE 1: 基础记忆读写 | |
| 121 | + // ═══════════════════════════════════ | |
| 122 | + console.log('┌──────────────────────────────────┐'); | |
| 123 | + console.log('│ PHASE 1: 基础记忆读写 │'); | |
| 124 | + console.log('└──────────────────────────────────┘'); | |
| 125 | + | |
| 126 | + // ── 1a. 第一次对话 ── | |
| 127 | + console.log('\n📋 1a. 发送第一次对话:让 Poet 写一首关于春天的五言绝句'); | |
| 128 | + const r1 = await post(`${BASE}/api/agent/task/dispatch`, { | |
| 129 | + from: 'master', to: AGENT, | |
| 130 | + task: '请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。', | |
| 131 | + priority: 'P2', autoExecute: true, | |
| 132 | + }); | |
| 133 | + test(r1.success, `Dispatch 成功 (taskId: ${r1.taskId})`); | |
| 134 | + const conv1 = r1.executionConvId; | |
| 135 | + | |
| 136 | + console.log(' ⏳ 等待对话完成...'); | |
| 137 | + const meta1 = await waitForStatus(AGENT, conv1); | |
| 138 | + test(meta1.status === 'completed', `对话完成 (toolCalls: ${meta1.stats?.toolCallCount})`); | |
| 139 | + await sleep(2000); | |
| 140 | + | |
| 141 | + // ① session 文件 | |
| 142 | + const sessions1 = listDir(path.join(MEM, 'short-term')).filter(f => f.startsWith('session-')); | |
| 143 | + test(sessions1.length >= 1, `① short-term/ 有 ${sessions1.length} 个 session 文件`); | |
| 144 | + | |
| 145 | + // ② memory.md 更新 | |
| 146 | + const mem1 = readFile(path.join(MEM, 'memory.md')); | |
| 147 | + const hasFact = mem1.includes('[2026-') || !mem1.includes('(尚无记录)'); | |
| 148 | + test(hasFact, `② memory.md 已更新关键事实`); | |
| 149 | + | |
| 150 | + // ── 1b. 第二次对话(验证近期记忆注入) ── | |
| 151 | + console.log('\n📋 1b. 发送第二次对话:要求 Agent 回忆之前写了什么'); | |
| 152 | + const r2 = await post(`${BASE}/api/agent/task/dispatch`, { | |
| 153 | + from: 'master', to: AGENT, | |
| 154 | + task: '你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。', | |
| 155 | + priority: 'P2', autoExecute: true, | |
| 156 | + }); | |
| 157 | + test(r2.success, `Dispatch 成功 (taskId: ${r2.taskId})`); | |
| 158 | + const conv2 = r2.executionConvId; | |
| 159 | + | |
| 160 | + console.log(' ⏳ 等待对话完成...'); | |
| 161 | + const meta2 = await waitForStatus(AGENT, conv2); | |
| 162 | + test(meta2.status === 'completed', `对话完成`); | |
| 163 | + await sleep(2000); | |
| 164 | + | |
| 165 | + // ③ 验证 Agent 引用了之前的内容 | |
| 166 | + const log2Path = path.join(AGENT_DIR, 'conversations', conv2, 'messages.log'); | |
| 167 | + const log2 = readFile(log2Path); | |
| 168 | + const remembered = log2.includes('春') || log2.includes('踏青') || log2.includes('诗') || log2.includes('记忆'); | |
| 169 | + test(remembered, `③ Agent 在第二次对话中引用了之前的春天/踏青内容`); | |
| 170 | + | |
| 171 | + const sessions2 = listDir(path.join(MEM, 'short-term')).filter(f => f.startsWith('session-')); | |
| 172 | + test(sessions2.length >= 2, `short-term/ 现在有 ${sessions2.length} 个 session 文件`); | |
| 173 | + | |
| 174 | + // ═══════════════════════════════════ | |
| 175 | + // PHASE 2: 海马体检索 + 小脑 | |
| 176 | + // ═══════════════════════════════════ | |
| 177 | + console.log('\n┌──────────────────────────────────┐'); | |
| 178 | + console.log('│ PHASE 2: 海马体检索 + 小脑固化 │'); | |
| 179 | + console.log('└──────────────────────────────────┘'); | |
| 180 | + | |
| 181 | + // ④ 保存小脑方案 | |
| 182 | + console.log('\n📋 2a. 保存一个"写诗"小脑方案'); | |
| 183 | + const planRes = await post(`${BASE}/api/agent/${AGENT}/memory/cerebellum`, { | |
| 184 | + scenario: '写古典诗词', | |
| 185 | + keywords: ['写诗', '诗词', '古诗', '五言', '七言', '绝句', '律诗', '创作'], | |
| 186 | + conditions: ['需要创作古典诗词', '需要遵循格律'], | |
| 187 | + steps: [ | |
| 188 | + '确认体裁(五绝/七绝/五律/七律/词)', | |
| 189 | + '确认主题和意境', | |
| 190 | + '起句: 点明时间/地点/情境', | |
| 191 | + '承句: 展开描写,丰富画面', | |
| 192 | + '转句: 转折或深化情感', | |
| 193 | + '合句: 收束全诗,升华主题', | |
| 194 | + ], | |
| 195 | + notes: ['五绝20字,七绝28字', '注意平仄和押韵', '避免直白说理,用意象表达'], | |
| 196 | + confidence: 0.9, | |
| 197 | + }); | |
| 198 | + test(planRes.success, `④ 小脑方案保存成功`); | |
| 199 | + | |
| 200 | + // 验证 hippocampus 索引 | |
| 201 | + const hippIdx = readFile(path.join(MEM, 'hippocampus-index.json')); | |
| 202 | + test(hippIdx.includes('写古典诗词'), `hippocampus-index.json 包含新条目`); | |
| 203 | + | |
| 204 | + // ⑤ 发送匹配任务 → 验证海马体命中 | |
| 205 | + console.log('\n📋 2b. 发送"写诗"任务 → 验证海马体检索'); | |
| 206 | + const r3 = await post(`${BASE}/api/agent/task/dispatch`, { | |
| 207 | + from: 'master', to: AGENT, | |
| 208 | + task: '请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。', | |
| 209 | + priority: 'P2', autoExecute: true, | |
| 210 | + }); | |
| 211 | + test(r3.success, `Dispatch 成功 (taskId: ${r3.taskId})`); | |
| 212 | + const conv3 = r3.executionConvId; | |
| 213 | + | |
| 214 | + console.log(' ⏳ 等待对话完成...'); | |
| 215 | + await waitForStatus(AGENT, conv3); | |
| 216 | + await sleep(2000); | |
| 217 | + | |
| 218 | + // ⑥ 验证使用次数递增 | |
| 219 | + // 海马体 useCount 在 buildSystemPrompt 时更新,需等服务端写完文件 | |
| 220 | + await sleep(5000); | |
| 221 | + const hippRaw = readFile(path.join(MEM, 'hippocampus-index.json')); | |
| 222 | + console.log(` 🔍 hippocampus raw: ${hippRaw.substring(0, 200)}`); | |
| 223 | + const hippIdxAfter = JSON.parse(hippRaw || '[]'); | |
| 224 | + const poetryEntry = hippIdxAfter.find(e => e.scenario === '写古典诗词'); | |
| 225 | + console.log(` 🔍 poetryEntry: ${JSON.stringify(poetryEntry)}`); | |
| 226 | + test(poetryEntry && poetryEntry.useCount >= 1, `⑥ 海马体使用次数: ${poetryEntry?.useCount} (期望 ≥1)`); | |
| 227 | + test(poetryEntry && poetryEntry.lastUsed !== null, `lastUsed 已更新: ${poetryEntry?.lastUsed}`); | |
| 228 | + | |
| 229 | + // ═══════════════════════════════════ | |
| 230 | + // PHASE 3: 反思 + 合并 + 日报 | |
| 231 | + // ═══════════════════════════════════ | |
| 232 | + console.log('\n┌──────────────────────────────────┐'); | |
| 233 | + console.log('│ PHASE 3: 反思 + 合并 + 日报 │'); | |
| 234 | + console.log('└──────────────────────────────────┘'); | |
| 235 | + | |
| 236 | + // ⑦ 模拟复杂对话触发反思 | |
| 237 | + console.log('\n📋 3a. 模拟复杂对话 → 验证自动反思'); | |
| 238 | + const { autoReflect } = require('../../server/services/memory-service'); | |
| 239 | + const complexMeta = { | |
| 240 | + status: 'completed', | |
| 241 | + stats: { toolCallCount: 8, messageCount: 4 }, | |
| 242 | + createdAt: new Date(Date.now() - 180000).toISOString(), | |
| 243 | + updatedAt: new Date().toISOString(), | |
| 244 | + title: '为国庆节创作一组主题诗集,包含5首诗', | |
| 245 | + }; | |
| 246 | + const complexOutput = `创作完成!以下是创作步骤: | |
| 247 | +1. 确定国庆主题和5个子题材(国旗、山河、英雄、和平、未来) | |
| 248 | +2. 分别确定每首诗的体裁(五绝、七绝、五律、七律、词) | |
| 249 | +3. 创作第一首《旗颂》五言绝句 | |
| 250 | +4. 创作第二首《山河赋》七言律诗 | |
| 251 | +5. 创作第三首《英雄辞》五言律诗 | |
| 252 | +6. 创作第四首《和平吟》七言绝句 | |
| 253 | +7. 创作第五首《展望》词·水调歌头 | |
| 254 | +8. 整体校对格律和意境 | |
| 255 | + | |
| 256 | +产出文件: | |
| 257 | +- workspace/poet/guoqing-collection.md | |
| 258 | +- workspace/poet/guoqing-notes.md`; | |
| 259 | + | |
| 260 | + const reflectResult = autoReflect(AGENT, 'test-complex', complexMeta, complexOutput); | |
| 261 | + test(reflectResult !== null, `⑦ 复杂对话触发了反思`); | |
| 262 | + test(reflectResult?.patterns?.length >= 1, `提取了 ${reflectResult?.patterns?.length} 个模式`); | |
| 263 | + test(reflectResult?.plan !== null, `自动固化了方案`); | |
| 264 | + | |
| 265 | + // 验证 reflections 目录 | |
| 266 | + const reflectFiles = listDir(path.join(MEM, 'reflections')).filter(f => f.startsWith('reflect-')); | |
| 267 | + test(reflectFiles.length >= 1, `reflections/ 有 ${reflectFiles.length} 个日志`); | |
| 268 | + | |
| 269 | + // ⑧ memory.md 更新 | |
| 270 | + const mem3 = readFile(path.join(MEM, 'memory.md')); | |
| 271 | + test(!mem3.includes('已验证的模式\n(尚无记录)'), `⑧ memory.md "已验证模式" 段有内容`); | |
| 272 | + | |
| 273 | + // 模拟失败对话 | |
| 274 | + console.log('\n📋 3b. 模拟失败对话 → 验证教训提取'); | |
| 275 | + const failReflect = autoReflect(AGENT, 'test-fail', { | |
| 276 | + status: 'failed', | |
| 277 | + stats: { toolCallCount: 4, messageCount: 2 }, | |
| 278 | + createdAt: new Date(Date.now() - 60000).toISOString(), | |
| 279 | + updatedAt: new Date().toISOString(), | |
| 280 | + title: '翻译古诗为英文但格式错误', | |
| 281 | + }, 'error: 输入格式无法解析 Error: Invalid poem format at line 3'); | |
| 282 | + | |
| 283 | + test(failReflect?.lessons?.length >= 1, `失败对话提取了 ${failReflect?.lessons?.length} 个教训`); | |
| 284 | + const mem3b = readFile(path.join(MEM, 'memory.md')); | |
| 285 | + test(!mem3b.includes('失败教训\n(尚无记录)'), `memory.md "失败教训" 段有内容`); | |
| 286 | + | |
| 287 | + // ⑨ 记忆合并 | |
| 288 | + console.log('\n📋 3c. 记忆合并 (short-term → long-term)'); | |
| 289 | + // 创建 3 个"过期"session(8天前) | |
| 290 | + const shortDir = path.join(MEM, 'short-term'); | |
| 291 | + const oldTs = Date.now() - 8 * 24 * 60 * 60 * 1000; | |
| 292 | + for (let i = 0; i < 3; i++) { | |
| 293 | + fs.writeFileSync(path.join(shortDir, `session-${oldTs + i}.md`), | |
| 294 | + `# Session 摘要\n\n- **时间**: 2026-03-17 0${i}:00:00\n\n## 任务\n旧任务-${i}: 写一首关于秋天的诗\n\n## 结果\n秋诗完成-${i}\n`, 'utf-8'); | |
| 295 | + } | |
| 296 | + | |
| 297 | + const shortBefore = listDir(shortDir).filter(f => f.startsWith('session-')); | |
| 298 | + console.log(` short-term/ 合并前: ${shortBefore.length} 文件`); | |
| 299 | + | |
| 300 | + const consolidateRes = await post(`${BASE}/api/agent/${AGENT}/memory/consolidate`, {}); | |
| 301 | + test(consolidateRes.merged === 3, `⑨ 合并了 ${consolidateRes.merged} 个过期 session (期望 3)`); | |
| 302 | + | |
| 303 | + const ltFiles = listDir(path.join(MEM, 'long-term')); | |
| 304 | + test(ltFiles.some(f => f.startsWith('consolidate-')), `long-term/ 有合并记录`); | |
| 305 | + | |
| 306 | + const shortAfter = listDir(shortDir).filter(f => { | |
| 307 | + const ts = parseInt(f.replace('session-', '').replace('.md', '')); | |
| 308 | + return !isNaN(ts) && ts < oldTs + 5; | |
| 309 | + }); | |
| 310 | + test(shortAfter.length === 0, `过期 session 已从 short-term/ 删除`); | |
| 311 | + | |
| 312 | + // ⑩ 日报 | |
| 313 | + console.log('\n📋 3d. 日报生成'); | |
| 314 | + const dailyRes = await post(`${BASE}/api/agent/${AGENT}/memory/daily-report`, {}); | |
| 315 | + test(dailyRes.success, `⑩ 日报生成成功`); | |
| 316 | + | |
| 317 | + const today = new Date().toISOString().substring(0, 10); | |
| 318 | + const dailyContent = readFile(path.join(MEM, 'daily', `${today}.md`)); | |
| 319 | + test(dailyContent.includes('# 日报'), `daily/${today}.md 包含日报标题`); | |
| 320 | + test(dailyContent.includes('对话数'), `日报包含统计数据`); | |
| 321 | + test(dailyContent.includes('反思摘要') || dailyContent.includes('反思数'), `日报包含反思信息`); | |
| 322 | + | |
| 323 | + // ═══════════════════════════════════ | |
| 324 | + // 最终汇总 | |
| 325 | + // ═══════════════════════════════════ | |
| 326 | + console.log('\n' + '═'.repeat(56)); | |
| 327 | + console.log('\n📊 Memory 目录最终状态:'); | |
| 328 | + const finalState = { | |
| 329 | + 'short-term/': listDir(path.join(MEM, 'short-term')).filter(f => f.endsWith('.md')).length, | |
| 330 | + 'long-term/': listDir(path.join(MEM, 'long-term')).length, | |
| 331 | + 'cerebellum/': listDir(path.join(MEM, 'cerebellum')).length, | |
| 332 | + 'reflections/': listDir(path.join(MEM, 'reflections')).length, | |
| 333 | + 'daily/': listDir(path.join(MEM, 'daily')).filter(f => f.endsWith('.md')).length, | |
| 334 | + 'memory.md': `${readFile(path.join(MEM, 'memory.md')).length} 字`, | |
| 335 | + 'hippocampus': hippIdxAfter.length + ' 条索引', | |
| 336 | + }; | |
| 337 | + for (const [k, v] of Object.entries(finalState)) { | |
| 338 | + console.log(` 📁 ${k.padEnd(18)} ${typeof v === 'number' ? v + ' 文件' : v}`); | |
| 339 | + } | |
| 340 | + | |
| 341 | + console.log('\n' + '═'.repeat(56)); | |
| 342 | + const status = failed === 0 ? '🎉 ALL PASSED' : '⚠️ SOME FAILED'; | |
| 343 | + console.log(`\n${status}: ${passed}/${total} 通过, ${failed} 失败\n`); | |
| 344 | + | |
| 345 | + if (failed > 0) process.exit(1); | |
| 346 | +} | |
| 347 | + | |
| 348 | +main().catch(err => { console.error('💥 测试崩溃:', err); process.exit(1); }); | ... | ... |
server/services/memory-service.js
| ... | ... | @@ -222,6 +222,7 @@ function retrieveRelevantPlans(agentId, taskText) { |
| 222 | 222 | const score = entryKeywords.length > 0 |
| 223 | 223 | ? hits.length / entryKeywords.length // 命中了 entry 的多少比例关键词 |
| 224 | 224 | : 0; |
| 225 | + console.log(`[HippocampusDebug] agent=${agentId} scenario="${entry.scenario}" score=${score} hits=${hits.length} taskKwLength=${taskKeywords.length} entryKws=${entryKeywords.length}`); | |
| 225 | 226 | return { ...entry, score, hits }; |
| 226 | 227 | }); |
| 227 | 228 | |
| ... | ... | @@ -440,35 +441,22 @@ const STOP_WORDS = new Set([ |
| 440 | 441 | |
| 441 | 442 | /** |
| 442 | 443 | * 从文本中提取关键词 |
| 443 | - * 策略: 中文按 2-4 字切片 + 英文按空格分词 + 去停用词 | |
| 444 | + * 取前 1500 字符,优先处理中文分词(重要任务内容),再处理英文,避免英文占位符号挤占中文。 | |
| 444 | 445 | */ |
| 445 | 446 | function extractKeywords(text) { |
| 446 | 447 | if (!text || text.length < 2) return []; |
| 447 | 448 | |
| 449 | + text = text.trim().substring(0, 1500); // 截断避免太长 | |
| 448 | 450 | const keywords = new Set(); |
| 449 | 451 | |
| 450 | - // 1. 英文词 (≥2字母) | |
| 451 | - const englishWords = text.match(/[a-zA-Z]{2,}/g) || []; | |
| 452 | - englishWords.forEach(w => { | |
| 453 | - const lower = w.toLowerCase(); | |
| 454 | - if (!STOP_WORDS.has(lower) && lower.length >= 3) { | |
| 455 | - keywords.add(lower); | |
| 456 | - } | |
| 457 | - }); | |
| 458 | - | |
| 459 | - // 2. 中文:提取连续中文字符序列,再分割成 2-4 字的 ngram | |
| 452 | + // 1. 中文:提取连续中文字符序列,再分割成 2-4 字的 ngram | |
| 460 | 453 | const chineseSegments = text.match(/[\u4e00-\u9fff]{2,}/g) || []; |
| 461 | 454 | for (const seg of chineseSegments) { |
| 462 | - // 完整段 (如果 ≤6 字) | |
| 463 | - if (seg.length <= 6 && !STOP_WORDS.has(seg)) { | |
| 464 | - keywords.add(seg); | |
| 465 | - } | |
| 466 | - // 2-gram | |
| 455 | + if (seg.length <= 6 && !STOP_WORDS.has(seg)) keywords.add(seg); | |
| 467 | 456 | for (let i = 0; i <= seg.length - 2; i++) { |
| 468 | 457 | const bi = seg.substring(i, i + 2); |
| 469 | 458 | if (!STOP_WORDS.has(bi)) keywords.add(bi); |
| 470 | 459 | } |
| 471 | - // 3-gram (如果段够长) | |
| 472 | 460 | if (seg.length >= 3) { |
| 473 | 461 | for (let i = 0; i <= seg.length - 3; i++) { |
| 474 | 462 | const tri = seg.substring(i, i + 3); |
| ... | ... | @@ -477,7 +465,14 @@ function extractKeywords(text) { |
| 477 | 465 | } |
| 478 | 466 | } |
| 479 | 467 | |
| 480 | - return [...keywords].slice(0, 30); // 最多 30 个关键词 | |
| 468 | + // 2. 英文词 (≥2字母) | |
| 469 | + const englishWords = text.match(/[a-zA-Z]{2,}/g) || []; | |
| 470 | + englishWords.forEach(w => { | |
| 471 | + const lower = w.toLowerCase(); | |
| 472 | + if (!STOP_WORDS.has(lower) && lower.length >= 3) keywords.add(lower); | |
| 473 | + }); | |
| 474 | + | |
| 475 | + return [...keywords].slice(0, 50); // 最多 50 个关键词 | |
| 481 | 476 | } |
| 482 | 477 | |
| 483 | 478 | // ════════════════════════════════════════ | ... | ... |