LLM提取器为什么不听话:一次424条事实标签缺失的根因诊断与Prompt修复工程

4 阅读5分钟

——一次retain_mission的MANDATORY TAGGING修复实录

TL;DR:AI记忆引擎的auto_retain功能在11天里默默存了424条事实,但所有事实都丢了topicstage标签。诊断发现: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_toolstopic:contenttopic: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里是可选项

这里有三种可能的根因:

  1. 指令强度不够:mission写了7个topic的描述,但用的是被动语态("business: monetization decisions"),没有用命令式("YOU MUST pick one topic for every fact")。
  2. 话题映射不明确:mission对每个topic的描述太抽象("infra: confirmed server/network/Docker configs"),但LLM面对"修改了Nginx server_tokens"这样的文本时,不容易映射到这个词。
  3. 缺少兜底规则: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关键词示例
businesspaid resources, 付费资源
dev_toolsnginx, WebUI, theme, sidebar, 工具站
infraserver specs, Docker, proxy, SSH, 服务器配置
contentarticle 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和敏感字段已脱敏。