hint 嵌进用户消息本体,后续所有 model call 白拿。连上一版的 turn-start 门控判断都省了 —— 每条消息最多查一次库,之后永远是零。
主路径 _handle_chat_message 和 edit-retry 路径都接了 _prepend_skill_hint;replay 重发专门注释了不重做(原内容已带 hint)。比图片引用做得好 —— 图片引用至今主路径和 regen 路径行为不一致。
查库发生在 WS handler 收消息时,用户等 LLM 之前就做完了。DB 抖动不再直接拖长首 token 延迟。
plat_editon.md §3.4 告诉模型把 reminder 里的 name 原样照抄、不许换相似 skill。get_skill_service(L45)、_get_viewer_user_id(L509)、typing 导入都在,没有悬空引用。
\\$ → \$ 不只是清理:原正则要求匹配一个字面反斜杠加行尾锚点,意味着 $token 以前根本匹配不上——这个触发功能等于从来没生效过,这次才是真启用。上线后 $xxx 的行为是全新的,值得盯一下日志。
"$" not in content 短路绝大多数消息在这里直接过去,零开销。
extract_skill_tokens(limit 5) → 逐个 svc.resolve()命中 → 生成一行"用户希望调用 xxx skill,id:…,name:…";查不到/禁用 → 静默跳过,load 工具兜底报错。
<system-reminder> 拼到用户消息最前面纯文本消息 → 前缀进 content 字符串;有图片引用 → 插为 content_array 第一个 text block。随 create_user_message 落库、落 checkpoint。
| 场景 | PG 查询 |
|---|---|
用户发 "$story-designer 帮我做分镜",该轮 4 次 model call | 入口 1 次,之后 0 |
下一轮 "继续做第二个场景"(无 $) | 0 |
| replay 重发老消息 | 0(原内容已带 hint,注释里写明了不重做) |
| edit 改文本后重发 | 重新解析 1 次(合理 —— 用户可能改了 $token) |
用户说 "预算 $100,单价 $5" | 4~6 条无效 SQL(问题①,还没修) |
$ + 纯数字仍会误触发查库这次的正则改动只修了转义 bug,没加字母约束。$100 / $5 依然被当成 token,每个 miss 打 2~3 条 SQL(by_id → by_name → user_skills 三连未命中),limit=5 下一条聊钱的消息最坏白烧 10~15 条 SQL,且没有任何缓存。
# skills/triggers.py:6 — token 必须至少含一个字母
- _TRIGGER_RE = re.compile(r"(?
+ _TRIGGER_RE = re.compile(r"(?
<system-reminder>关键事实:前端显示读的是 session message(state_manager.add_message 存的那份),不是 checkpoint。而现在同一个被污染的 content 流向了三个地方:
| 去向 | 需要 hint 吗 | 现状 |
|---|---|---|
create_user_message → add_message → session message(前端显示源) | ❌ 不需要 | 被污染 |
同一对象传给 process_user_message → checkpoint / LLM 历史 | ✅ 唯一需要的地方 | 正确 |
_generate_session_name_from_message(session_id, content) → 自动会话标题 | ❌ 不需要 | 连带 bug:标题可能从 reminder 文本生成 |
# 1) 存库 / 前端展示 / 标题生成:一律用原始内容
user_message = create_user_message(session_id, content, metadata=metadata, content_array=content_array)
await self.state_manager.add_message(session_id, user_message)
# 2) 只给 agent 的那份带 hint(进 checkpoint,前端永远看不到)
agent_content, agent_array = await self._prepend_skill_hint(
connection_id, content, content_stripped, content_array)
agent_message = copy.deepcopy(user_message) # 保持同一 message id
agent_message.payload.content = agent_content
if agent_array is not None:
agent_message.payload.content_array = agent_array
await self.agent_orchestrator.process_user_message(session_id, agent_message)
这一拆的三个自动收益:前端不用改(session message 永远干净);问题 ④ 消失(编辑框回填干净文本);标题 bug 消失。
_prepend_skill_hint(用户原文里 $token 还在,重新 resolve 天然可行)。三个路径统一成"构建 agent-bound 消息时调 helper",心智模型最简单。不补也能跑:基础 prompt §3.4 本来就教模型看到 $token 主动 load,hint 只是加固。
resolve() 同步 PG 调用,照样卡事件循环svc.resolve() 内部走 psycopg 同步连接池,现在跑在 async 的 WS handler 里 —— 阻塞期间所有 session 的 WebSocket 消息都排队。移出 model call 关键路径让用户感知变小了,但事件循环层面的问题没消失。
# websocket_handler.py · _build_skill_hint_text 里
- res = svc.resolve(viewer_user_id=viewer, token=tok, require_enabled=True)
+ res = await asyncio.to_thread(
+ svc.resolve, viewer_user_id=viewer, token=tok, require_enabled=True
+ )
用户编辑一条已带 hint 的旧消息时,编辑框回填存储内容(含旧 hint),提交后 edit 路径再 prepend 一层新 reminder → 双层嵌套。
按 ③ 的"存干净的、传增强的"改完之后,存储内容里不再有 hint,这个问题不需要单独处理。
上一版走的是 pre_model_hook + 门控(方案 C),门控本身是对的。换成 Plan A 后的得失:
| 维度 | Plan A · 入口处(当前实现) | Plan C · pre_model_hook(上一版) |
|---|---|---|
| 查库次数 | 每条消息 1 次,永久生效 | 每个含 $ 的轮 1 次(每轮重判) |
| 查库位置 | 不在 model call 关键路径 | model call 前,DB 慢拖长首 token |
| 入口数量 | 2 处(主路径 + edit),✅ 都已覆盖 | 1 处天然全覆盖 |
| 信息新鲜度 | 冻结在消息里,skill 改名/禁用后旧提示过时(影响小) | 每轮现查,永远最新 |
| 污染用户消息 | 会 → 问题③④ | 不会(只进 llm_input_messages) |
| prompt cache | 无打断 | 每轮尾部打断一次 |
| 事件循环阻塞 | 两边都要 asyncio.to_thread,位置换了问题不消失(问题②) | |
| # | 动作 | 文件 | 必要性 |
|---|---|---|---|
| 1 | 正则加"至少含一个字母",挡掉 $100 误触发 | skills/triggers.py:6 | 合并前 |
| 2 | 拆两份:"存干净的、传增强的" —— session message / 标题生成用原文,只有传给 orchestrator 的副本带 hint。前端零改动,问题 ④ 和标题 bug 一并消失 | websocket_handler.py(主路径 + edit 路径) | 合并前 |
| 3 | replay 路径补调 _prepend_skill_hint(拆完后存储内容不再带 hint,原注释前提失效) | websocket_handler.py · replay 分支 | 跟 2 一起 |
| 4 | resolve 套 asyncio.to_thread | websocket_handler.py · _build_skill_hint_text | 强烈建议 |
| 5 | 上线后盯 [skill_hint] 日志 —— 这功能以前因正则 bug 从未生效,行为是全新的 | — | 观察项 |
入口覆盖、replay 处理、prompt 配套、引用完整性这几处都验过没问题,架构不用再动。