——一次retain_mission的MANDATORY TAGGING修复实录
TL;DR:AI记忆引擎的auto_retain功能在11天里默默存了424条事实,但所有事实都丢了
topic和stage标签。诊断发现:retain_mission只告诉LLM"存什么",没告诉它"必须打标签"。修复方案:在mission最前面插入MANDATORY TAGGING块,用3条规则强制标注——零数据损失,新事实全部自动带标签。
一、问题:424条事实,全部缺标签
一天做数据审计时,跑了条SQL:
SELECT count(*) FROM memory_units
WHERE bank_id='hermes'
AND NOT EXISTS (SELECT 1 FROM unnest(tags) AS t WHERE t LIKE 'topic%');
返回结果:424。再查stage标签,同样424条缺失。
这意味着从6月12日到6月23日这11天里,auto_retain存进记忆库的每一批事实,都没有topic和stage分类。
进一步定位发现:全部424条都来自同一个会话,tags数组里只有一个字段——session:cad9c1bfc9d6。不是空数组,不是格式错误,是LLM提取器根本没打标签。
而这些事实的内容呢?不是噪音。是正经的业务内容:
- Nginx配置变更(
server_tokens off) - CSDN付费资源发布记录
- WebUI主题切换
- 工具站路由重构
本该打上topic:dev_tools、topic:content、topic:business的事实,全部裸奔了。
二、诊断:LLM推理 vs 规则执行的张力
当时系统里retain_mission的配置是这样的(简化版):
【STORE - worth retaining】
- business: monetization decisions...
- infra: confirmed server/network/Docker configs...
- dev_tools: final Hermes/Skills/AI tool configurations...
...
【Rule】
- Is this turn searching for an answer or has the answer been decided?
Former → skip. Latter → store.
- When unsure, skip.
问题在哪?
这段mission只定义了"存什么"的领域指引,没有一句说"每条事实都必须打topic标签"。
LLM提取器面对一段对话,会判断"这条值得存"并生成事实文本——但它没有收到"同时必须挑选一个topic并打上去"的强制指令。对LLM来说,topic字段在这个mission里是可选项。
这里有三种可能的根因:
- 指令强度不够:mission写了7个topic的描述,但用的是被动语态("business: monetization decisions"),没有用命令式("YOU MUST pick one topic for every fact")。
- 话题映射不明确:mission对每个topic的描述太抽象("infra: confirmed server/network/Docker configs"),但LLM面对"修改了Nginx server_tokens"这样的文本时,不容易映射到这个词。
- 缺少兜底规则:mission没有"如果实在不确定归哪个topic,就用XXX"的catch-all机制——LLM不确定时选择跳过。
本质矛盾:retain_mission是一段规则文本,但LLM处理它是语义推理。你在文本里写了"topic分成7类",LLM理解的是"按语义归类的参考"。你需要的是"每条事实必须选一个",它没有接收这个信号。
三、修复:MANDATORY TAGGING块设计
修复思路:在STORE之前插入一个强制标注块,把"必须打标签"从暗示变成显式命令。
只改了1处,加了约800字符。完整的新block:
【MANDATORY TAGGING — every extracted fact MUST have exactly one topic tag】
Pick the SINGLE best topic from the 7 below. Never skip the topic field.
topic:business — monetization, pricing, revenue, paid resources
topic:dev_tools — tool config, routing, nginx, WebUI, theme, CSS, sidebar, GitHub
topic:infra — server specs, deployment, Docker, proxy, ports, SSH
topic:content — article publishing, cross-platform, article posting
topic:code — bug fixes, code changes, scripts, Python
topic:research — market analysis, competitor research, tech selection
topic:reflection — catch-all when none of the above fit
CRITICAL: If unsure between two topics, pick the closer one.
If NO topic matches, use topic:reflection.
The topic field must NEVER be empty.
三个关键设计:
1. 置顶位置
放在STORE段之前——LLM处理长prompt时对开头注意力最高。MANDATORY TAGGING是第一段实质性内容,LLM不会略过。
2. 中文关键词映射
每个topic后面不止有英文描述,还加了中文关键词:
| topic | 关键词示例 |
|---|---|
| business | paid resources, 付费资源 |
| dev_tools | nginx, WebUI, theme, sidebar, 工具站 |
| infra | server specs, Docker, proxy, SSH, 服务器配置 |
| content | article publishing, cross-platform, 文章发布 |
这不是为了让mission更长——是为了降低LLM的映射成本。面对"修改了Nginx的server_tokens配置"这样的文本,LLM能在关键词列表里直接命中dev_tools: nginx。
3. catch-all兜底
topic:reflection作为最后防线。LLM不需要在"确定归哪个topic"和"跳过标注"之间二选一——不确定时就用reflection,tag字段永远不会空。
四、验证:零数据损失 + 未来事实自动带标签
PATCH配置的验证流程:
# 1. PATCH前抓基线
BEFORE=$(curl -s '<hindsight-api>/banks/hermes/stats' | jq '.total_nodes')
# → 774
# 2. PATCH retain_mission
curl -s -X PATCH '<hindsight-api>/banks/hermes/config' \
-H 'Content-Type: application/json' \
-d '{"updates":{"retain_mission":"<new_mission>"}}'
# 3. PATCH后立即对比
AFTER=$(curl -s '<hindsight-api>/banks/hermes/stats' | jq '.total_nodes')
# → 774(delta=0)
关键点:改retain_mission不会触发数据重建——它只是替换LLM看到的文字指令。total_nodes从PATCH前到PATCH后完全没变。
对已经入库的424条漏标事实,用了一次性SQL补标签(关键词匹配 + reflection兜底)。对未来的新事实:下一次auto_retain触发时,LLM拿到的是新mission,MANDATORY TAGGING块在最高优先级位置。
五、启示:LLM指令设计的3个教训
教训1:描述 ≠ 命令
"business: monetization decisions"是描述。"Pick the SINGLE best topic and NEVER skip"是命令。
LLM对描述性文本处理方式是"理解并参考";对命令式文本是"理解并执行"。如果想让LLM执行某个动作,用命令式。
教训2:关键词映射比分类描述更有效
"infra: confirmed server/network/Docker configs"给了LLM一个分类概念。但LLM看到"修改了Nginx server_tokens"时,需要从零开始做语义映射。
"infra — nginx, Docker, proxy, SSH"直接给了LLM一个词典,O(1)命中。
教训3:永远给兜底规则
没有catch-all的后果:LLM不确定 → 跳过 → 字段为空 → 424条漏标。
有catch-all的后果:不确定 → topic:reflection → 字段有值 → 0条漏标。
差别只在最后一行。
附录:完整修复SQL
对历史漏标事实的补救脚本(已执行):
-- 关键词匹配补 topic
UPDATE memory_units SET tags = array_append(tags, 'topic:dev_tools')
WHERE bank_id='hermes'
AND NOT EXISTS (SELECT 1 FROM unnest(tags) AS t WHERE t LIKE 'topic%')
AND (text ILIKE '%nginx%' OR text ILIKE '%路由%' OR text ILIKE '%WebUI%');
-- content/business/infra/code 同理...
-- 兜底:剩余全部归 reflection
UPDATE memory_units SET tags = array_append(tags, 'topic:reflection')
WHERE bank_id='hermes'
AND NOT EXISTS (SELECT 1 FROM unnest(tags) AS t WHERE t LIKE 'topic%');
-- 补 stage
UPDATE memory_units SET tags = array_append(tags, 'stage:reference')
WHERE bank_id='hermes'
AND NOT EXISTS (SELECT 1 FROM unnest(tags) AS t WHERE t LIKE 'stage%');
最终状态:774条事实,0缺topic,0缺stage。
本文基于真实AI记忆引擎运维经验撰写。所有数据均为实测,IP和敏感字段已脱敏。