2026-09-23·by Sijie Wang#standmeet#architecture#design

ghost-steering

幽灵引导——一条建议消息,弯转对话方向

父节点:agent-core · 状态:已构建,并在真实模型上验证过(2026-07-16)。策略层、waypoint 冻结和已访问台账都已存在且能工作;waypoint 按角色(role)和按 code 分别著作(code 继承其角色的 waypoint,可按 waypoint_id 覆盖)。面向 owner 的编辑器后来也落地了(role-waypoints-admin e2e),所以下文那条元原则——"owner 只写目的地"——现在有可供书写的界面了。

Owner 希望访客对话向按角色设定的目的地收敛(查看某个项目、理解差异化、预约一次通话)——但议程是访客自己的,不存在硬控制。唯一可用的工具是一条建议通道:幽灵消息(ghost message)。这是 steering-without-control 的一个实例:一个场,而不是一道围栏。

四根理论支柱

  1. 选择架构 / 助推(Thaler–Sunstein)。 不约束选项集合,而是塑造默认项。单条幽灵消息是最强的形式:为下一步动作提供一个纯粹的默认值。(Hick 定律 / 选择过载:菜单会稀释注意力,默认项才促成转化。这正是一条幽灵胜过三条的原因——界面直觉和理论在此一致。)
  2. 带建议偏置的 MDP。 对话 = 状态(已覆盖的话题、已展示的证据、已达成的目标);访客的下一条消息 = 动作。幽灵消息并不改变动作空间——它偏置转移核(被提供的文本获得更高的接受概率)。Owner 的目的地被编译成一个奖励 R;幽灵策略挑选让向 R 推进的期望值最大化的候选项。
  3. 目标导向对话(Target-guided dialogue,Tang 等人 2019,以及主动式对话这一研究线)。已发表文献中的核心张力是:局部连贯性 × 全局进展——不连贯的跳转是文献记录在案的失败模式。下文机械化的"挂钩"规则正是为此而设。
  4. 管道 / waypoint 视角(我们自己的提法)。 Owner 声明的waypoint 是期望对话走廊的横截面;幽灵消息是一道软门——它从不阻拦,只是把对话重新拉回中心。用势场的形式表达:φ(state) = 到未访问 waypoint 的加权距离;每条幽灵消息都在连贯性约束下最大化期望的 φ 下降量;一个无视它的访客感受到的力恰好为零。

只留一条幽灵的决定(为什么它更强,而不只是更友好)

  • 默认项胜过菜单——见支柱一。
  • 探索发生在多轮之间:这把 bandit 问题变成了序贯的。一条被拒绝的幽灵消息(被无视 / 访客自己打字)是对该 waypoint 的一个干净的负信号——若是三选一菜单,你分不清"拒绝了 A"还是"只是更偏好 B"。
  • 单一位置迫使出两条规则,使策略读起来像引导而不是推销:
    • 动量(柔道式引导)——如果访客自己的消息已经指向了某个未访问的 waypoint,就顺势推那一个;借力于水流,而不是与之对抗;
    • 沉默也是一种动作——没有自然的挂钩点,或没有可推进的事项 ⟹ 什么都不产生。过度助推会训练出banner盲视和逆反心理;稀缺性才能保住默认项的力量。

元原则:owner 只写目的地,不写机制

系统提示词是平台所有、稳定、带版本的(part_ids + hash——适用 system-prompt-hash-regression 的纪律)。Owner 按角色著作:waypoint(描述、权重、evidence_refs)、语气约束、禁区——像其他一切一样被冻结进 RoleSnapshot(role-snapshot-frozen)。依据:权力分离(owner 擅长声明目标、不擅长写机制提示词——这是隐性知识与可书写内容之间的裂缝),而稳定的机制是做 A/B 测试和回归测试的前提。

提示词骨架(单幽灵版)

You generate at most ONE GHOST MESSAGE: a candidate next message the
VISITOR might send — written in the visitor's voice, not the owner's.

INPUTS per turn:
- role: {persona, tone constraints, forbidden zones}
- waypoints: [{id, description, weight, evidence_refs, visited?}]
- conversation summary + the assistant's LAST message
- ghost history: [{waypoint, offered_at, accepted?}]

RULES:
1. VOICE — something a curious {role} would plausibly say next:
   short, first-person, their register. Never marketing, never pressure.
2. COHERENCE — it must hook onto something concrete in the
   assistant's last message (name what it follows from).
3. HEADING — tag it with exactly ONE unvisited waypoint it advances.
   No tag → do not emit.
4. ONE OR NONE — if no natural hook advances any unvisited waypoint,
   emit NOTHING. Silence is an action.
5. SELECTION — follow the visitor's momentum when their own message
   already points at an unvisited waypoint; otherwise the highest-
   weight waypoint coherently reachable in one hop; bridge if none.
6. EVIDENCE — only propose questions the corpus can answer well
   (waypoints carry evidence_refs; no refs → not proposable).
   <!-- Rule 6 was prompt-only until 2026-07-23 (the real model ignored it: a waypoint with
        evidence_refs=null still produced three ghosts, 2026-07-16). Enforced since a5919835c by the
        `require_ghost_evidence` switch (F-A-10; per-role column, nullable per-code override, frozen
        into RoleSnapshot.RequireGhostEvidence): when on, `conversation.SteeringCandidates`
        (routes/public/agent_turn.go:100) drops non-terminal waypoints with no evidence_refs before the
        policy call; terminals stay proposable. A second floor (F-A-26, usecase/visitor_waypoint_feasible.go)
        drops refs that resolve to no note at freeze. -->
7. NO-REPEAT — a declined waypoint may not be re-offered for N turns.
8. TERMINAL — when booking/contact is one natural step away, the slot
   goes to the ask, plainly.

OUTPUT: {text, target_waypoint, follows_from, is_bridge} | null

逐条依据

条款依据
voice(语态)言语行为的归属——点击即等于把这句话据为己有;营销腔调会触发逆反(Brehm)并扼杀接受率
coherence + follows_from(连贯性)目标导向对话文献记录在案的失败模式;把挂钩点写明使连贯性变得可机械检验(检验会产生梯度信号)
heading tag(标注 waypoint)root-protocol 中"声明你的航向"条款逐字一致:一个说不出自己推进了什么的动作就是在漫游;这个标签同时也是遥测的挂钩点
one-or-none(一条或不产生)稀缺性才能保住默认项的力量;过度助推 → banner 盲视 + 逆反
momentum(动量)steering-without-control:借力于当前的水流;顺着已显露的兴趣去引导,比硬拽向最高权重转化率更高
evidence gate(证据门)可行性下限:一条指向语料稀薄之处的幽灵消息会把对话引向失败;是"有据可查、带引用"这一承诺的上游守卫
no-repeat(不重复)序贯 bandit:一次拒绝就是负奖励;立刻重新提供只会白白消耗信任、换不来任何信息
terminal(终点)转化漏斗需要一个明确的收口动作;对应对话层面的 force-final-answer
JSON output(JSON 输出)对结构的机械化约束;target_waypoint 会喂给漏斗统计;follows_from 是审计轨迹

代码设计——它落在现有架构中的哪个位置

今天已经存在的(已验证): ghost 目前已经是一种静态机制——access_codes.ghosts jsonb 为每个 code 保存一份 owner 著作的建议列表,conversation_ghosts 记录每一次展示:{id, owner_id, conversation_id, turn_index, ghost_text, source, shown_at, accepted_at}。接受与否已经是一个时间戳accepted_at,NULL 表示被拒绝/待定),source 也已经在标注来源。本设计要做的,是把 ghost 从一份静态的按 code 罗列的清单升级为一套策略——source 在原有静态值之外新增一个 policy 值。

数据(沿用已有的 role-join 模式——全部已在 backend/db/schema.sql 里):

  • role_waypoints——挨着 role_corpus_uris/role_skills 的连接表:{role_id, waypoint_id, description, weight, evidence_refs jsonb, is_terminal}(schema.sql:508);在管理后台的 Roles 部分著作。它的 per-code 镜像 code_waypoints(schema.sql:498)由 access.MergeWaypoints 叠在它之上(同 waypoint_id → code 覆盖,新 id → 追加),叠加发生在语料过滤之前
  • conversation_ghosts 带有 {target_waypoint, follows_from}(schema.sql:1105)——即航向标签和连贯性挂钩,搭载在已有的接受度机制之上;策略产出的 ghost 是 source='policy'
  • RoleSnapshot 带有 Waypoints + RequireGhostEvidenceinternal/access/entity/role_snapshot.go:94,挨着它已有的那些被冻结字段——它已经在冻结 codePromptBodydockButtons,所以这个模式是现成的)——并且在冻结时过滤:一个 evidence_refs 落在该角色被授予的语料 glob(AllowsCorpus)之外的 waypoint,会在门口就被剔除(access.FilterWaypointsByCorpusentity/waypoint.go:143)。可行性在结构层面被强制:一个角色永远不会被引导向它看不到的证据。

"已访问"标记是纯机械的(α≈0——台账里没有 LLM 裁判):

  • 证据类 waypoint 在 assistant 的引用触及其 evidence_refs 时翻转为已访问(每轮的 grounding 本就已被追踪);
  • 动作类 waypoint工具事件上翻转(例如来自预约插件的预约成功)。 waypoint 台账搭载在 Redis 的 visitor_session 上(VisitedWaypointsinternal/conversation/usecase/waypoint_ledger.go)。

策略调用——一个持久化之后的钩子: 生成 ghost 需要拿到完整的回复,所以它在完成后持久化(persist-at-completion)之后、作为本轮的尾声运行(done = 已提交,detached-turn-persist-at-completioninference.EpilogueFunc,在 routes/public/agent_turn.go:64 buildGhostForTurn 接入——只在 code 模式 + 冻结了 waypoint 时):SteeringCandidates → 用 conversation.GhostPolicyPromptinference.GenerateParseGhost → 落库 → 出帧。已建成的版本跑在会话自己的凭据/模型上(agent_turn.go:106),并没有单独的廉价档位——"廉价/快速档位"仍是尚未实现的意图;其机制提示词是一个带 part_ids + hash 的带版本片段usecase/ghost_policy.go:29;适用 system-prompt-hash-regression——引导的漂移就是提示词的漂移)。未访问集合为空时在任何 LLM 调用之前就短路。

传输与接受检测: 一种 SSE 帧类型 ghost,在 done 之后发出(就是 inference/agent_epilogue.go 里通用的 EpilogueFrame{Kind:"ghost"}——循环只认识 Kind;unbuffered-sse-passthrough 像传输其他帧一样传输它);前端把 ghost 文本渲染进输入框(Tab 键 / 点击接受)。接受 ⟹ 发送的消息携带 ghost_idaccepted=true;任何其他用户消息 ⟹ 待定的 ghost 翻转为 accepted=false(不重复窗口开始计时)。

评测接口: GhostPolicy 是一个作用于 (snapshot, transcript, history) 的纯函数——可以被 eval-harness 用预设好的对话记录和标准 waypoint 序列直接驱动,不需要新的基础设施。

类图

带 ghost 的单轮流程(时序图)

Waypoint 与 ghost 的生命周期(状态图)

闭环(否则它只是空中楼阁)

  • 离线(已建成):eval-harness 通过它的 Driver,用预设对话驱动同一个循环;scenarios/ghost-momentum.yml / ghost-silence.ymlexpect_ghost 标准答案(scenario.goExpectGhostEmitted=false 断言沉默)。
  • 在线遥测: 按每条 ghost 记录(是否提供、是否接受、哪个 waypoint)→
    • 各 waypoint 的接受率——已建成,2026-07-07(7faadc507):conversations.ghost_telemetryusecase/ghosts.go:85 GhostTelemetryGhostWaypointStat{TargetWaypoint, Shown, Accepted} + AcceptanceRate()),显示在 Conversations 区块的 GhostTelemetryPanel.tsx;e2e ghost-telemetry.spec.ts / ghost-telemetry-admin.spec.ts。访客真正想要的是哪些目的地:这是给 owner 关于自己叙事拉力的真实反馈(这本身就是一个产品时刻);
    • 每个对话的 waypoint 漏斗;到达终点所需轮数;游荡轮数——未建成(GhostWaypointStat 里没有按对话的漏斗);
    • 沉默率——策略健康度指标:过低 = 在推销,过高 = waypoint 设置得太单薄——未建成(沉默的一轮不写 conversation_ghosts 行,所以没有东西在数它)。
  • UI 上的解锁:单条 ghost 恰好契合 ghost-text 模式(灰色的行内文字,Tab 键接受)——比按钮更安静,与"阅读优先"的立场一致。
about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

ghost-steering